@c6fc/spellcraft 0.1.1 → 0.1.3

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 CHANGED
@@ -2,192 +2,169 @@
2
2
 
3
3
  **The Sorcerer's Toolkit for Unified Configuration Management.**
4
4
 
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.).
5
+ SpellCraft is a plugin framework for [Jsonnet](https://jsonnet.org/) that bridges the gap between declarative configuration and the Node.js ecosystem. It allows you to import NPM packages directly into your Jsonnet logic, execute native JavaScript functions during configuration generation, and manage complex infrastructure-as-code requirements from a single, gnostic workflow.
6
6
 
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.
7
+ SpellCraft provides a single, unified source of truth, letting you orchestrate any tool that needs machine-readable configurations (like Terraform, Packer, Kubernetes, or Ansible) from one place.
8
8
 
9
9
  [![NPM Version](https://img.shields.io/npm/v/@c6fc/spellcraft.svg)](https://www.npmjs.com/package/@c6fc/spellcraft)
10
- [![License](https://img.shields.io/npm/l/@c6fc/spellcraft.svg)](https://github.com/your-repo/spellcraft/blob/main/LICENSE)
10
+ [![License](https://img.shields.io/npm/l/@c6fc/spellcraft.svg)](https://github.com/c6fc/spellcraft/blob/main/LICENSE)
11
11
 
12
12
  ---
13
13
 
14
14
  ## The SpellCraft Philosophy
15
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.
16
+ 1. **Declarative Power (Jsonnet):** Configurations are written in Jsonnet. Variables, functions, and inheritance allow you to define components once and reuse them everywhere.
17
+ 2. **Native Node.js Resolution:** No custom registries. No hidden magic. SpellCraft modules are just NPM packages. If you can `npm install` it, SpellCraft can load it.
18
+ 3. **Scoped Extensibility:** Native JavaScript functions are automatically namespaced based on their package name, ensuring that dependencies never clash, even if multiple modules use different versions of the same library.
23
19
 
24
20
  ## Quick Start
25
21
 
26
- Get up and running with SpellCraft in minutes.
27
-
28
22
  ### 1. Installation
29
23
 
30
- Install the SpellCraft CLI and core library into your project.
24
+ Install the CLI and core library.
31
25
 
32
26
  ```sh
33
27
  npm install --save @c6fc/spellcraft
34
28
  ```
35
29
 
36
- ### 2. Import a Module
30
+ ### 2. Install a Plugin
37
31
 
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.
32
+ Install a SpellCraft-compatible plugin using standard NPM.
39
33
 
40
34
  ```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'
35
+ npm install --save @c6fc/spellcraft-aws-auth
47
36
  ```
48
- This makes the `@c6fc/spellcraft-aws-auth` package available in your Jsonnet files under the name `awsauth`.
49
37
 
50
- ### 3. Create Your First Spell
38
+ ### 3. Write Your Spell
51
39
 
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.
40
+ Create a `manifest.jsonnet` file. Unlike previous versions of SpellCraft, you import modules explicitly using standard Node resolution.
53
41
 
54
- Create a file named `manifest.jsonnet`:
55
42
  ```jsonnet
56
- // manifest.jsonnet
57
- local modules = import 'modules';
43
+ // Import the library directly from node_modules
44
+ local aws = import '@c6fc/spellcraft-aws-auth/module.libsonnet';
58
45
 
59
46
  {
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(),
47
+ // Use functions provided by the module
48
+ 'aws-identity.json': aws.getCallerIdentity(),
63
49
 
64
- // We can also create YAML files. SpellCraft has built-in handlers for common types.
65
50
  'config.yaml': {
66
51
  apiVersion: 'v1',
67
52
  kind: 'ConfigMap',
68
- metadata: {
69
- name: 'my-app-config',
70
- },
53
+ metadata: { name: 'my-app-config' },
71
54
  data: {
55
+ // Use built-in native functions
72
56
  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,
57
+ callerArn: aws.getCallerIdentity().Arn,
75
58
  },
76
59
  },
77
60
  }
78
61
  ```
79
62
 
80
- ### 4. Generate the Artifacts
63
+ ### 4. Generate Artifacts
81
64
 
82
- Use the `generate` command to render your `.jsonnet` file into the `render/` directory.
65
+ Run the generator. SpellCraft automatically detects installed plugins in your `package.json`, registers their native functions, and renders your configuration.
83
66
 
84
67
  ```sh
85
68
  npx spellcraft generate manifest.jsonnet
86
69
 
87
70
  # 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
71
+ # [+] Evaluating Jsonnet file: .../manifest.jsonnet
92
72
  # [+] Writing files to: render
93
73
  # -> aws-identity.json
94
74
  # -> config.yaml
95
75
  # [+] Generation complete.
96
76
  ```
97
77
 
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
- ```
78
+ ---
109
79
 
110
- ## The SpellCraft CLI
80
+ ## Rapid Prototyping (Local Modules)
111
81
 
112
- The `spellcraft` CLI is your primary interface for managing modules and generating files.
82
+ Sometimes you need a custom function just for your current project, and you don't want to publish a full NPM package. SpellCraft provides a **Local Magic** folder for this.
113
83
 
114
- ### Core Commands
84
+ 1. Create a folder named `spellcraft_modules` in your project root.
85
+ 2. Create a JavaScript file, e.g., `spellcraft_modules/utils.js`:
115
86
 
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.
87
+ ```javascript
88
+ // spellcraft_modules/utils.js
89
+ exports.shout = (text) => text.toUpperCase() + "!!!";
90
+ exports.add = (a, b) => a + b;
118
91
 
119
- ### Extensible CLI
92
+ // Use standard functions to access 'this', which is extended by plugins:
93
+ exports.know_thyself = function() {
94
+ this.aws.getCallerIdentity()
95
+ }
96
+ ```
120
97
 
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.
98
+ 3. In your Jsonnet file, import `modules` to access your exported functions:
122
99
 
123
- Run `npx spellcraft --help` to see all available commands, including those added by modules.
100
+ ```jsonnet
101
+ // Import the automatically generated local module aggregator
102
+ local modules = import 'modules';
124
103
 
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 # Added by a module
133
- spellcraft aws-exportcredentials Export the current credentials as environment variables # Added by a module
104
+ {
105
+ 'test.json': {
106
+ // Access your local JS functions here
107
+ // Our file was named 'utils.js', so the exported
108
+ // functions are accessed via 'modules.utils'.
109
+ message: modules.utils.shout("hello world"),
110
+ sum: modules.utils.add(10, 5)
111
+ }
112
+ }
134
113
  ```
135
114
 
136
- ## Programmatic Usage (API)
115
+ ---
116
+
117
+ ## The SpellCraft CLI
118
+
119
+ The CLI is automatically extended by installed modules.
137
120
 
138
- For more advanced workflows, such as integration into larger automation scripts, you can use the `SpellFrame` class directly in your Node.js code.
121
+ * `spellcraft generate <filename>`: Renders a Jsonnet file to the `render/` directory.
122
+ * `spellcraft --help`: Lists all available commands, including those added by plugins (e.g., `spellcraft aws-identity`).
139
123
 
140
- The typical flow is:
141
- 1. Instantiate `SpellFrame`.
142
- 2. Run module initializers with `init()`.
143
- 3. Render the Jsonnet file with `render()`.
144
- 4. Write the resulting object to disk with `write()`.
124
+ ---
125
+
126
+ ## Programmatic API
127
+
128
+ You can embed SpellCraft into your own Node.js scripts for advanced automation.
145
129
 
146
130
  ```javascript
147
- // my-automation-script.js
148
131
  const { SpellFrame } = require('@c6fc/spellcraft');
149
132
  const path = require('path');
150
133
 
151
- // 1. Instantiate the SpellFrame
152
- // Options allow you to customize output paths, cleaning behavior, etc.
153
- const frame = new SpellFrame({
154
- renderPath: "dist", // Output to 'dist/' instead of 'render/'
155
- cleanBeforeRender: true,
156
- });
134
+ const frame = new SpellFrame();
157
135
 
158
136
  (async () => {
159
-
160
- // 2. Initialize modules before rendering.
137
+ // 1. Initialize: Scans package.json for plugins and loads them
161
138
  await frame.init();
162
139
 
163
- // 3. Render the master Jsonnet file
164
- const manifest = await frame.render(path.resolve('./manifest.jsonnet'));
165
-
166
- // The result is available in memory
167
- console.log('Rendered Manifest:', JSON.stringify(manifest, null, 2));
140
+ // 2. Render: Evaluates the Jsonnet
141
+ // Note: The result is a pure JS object
142
+ const result = await frame.render(path.resolve('./manifest.jsonnet'));
143
+ console.log(result);
168
144
 
169
- // 4. Write the manifest object to the filesystem. Defaults to the contents of
170
- // the most recent 'render'.
145
+ // 3. Write: Outputs files to disk (applying JSON/YAML transformations)
171
146
  frame.write();
147
+ })();
148
+ ```
172
149
 
173
- console.log('Successfully wrote files to the dist/ directory!');
150
+ ## Creating Modules
174
151
 
175
- })();
152
+ A SpellCraft module is simply an NPM package with specific metadata. You can get a head-start with:
153
+ ```bash
154
+ npm init spellcraft-module @your_org/your_module
176
155
  ```
177
156
 
178
- ## Top-tier spellframe extensions by SpellCraft authors:
157
+ Learn more at [**create-spellcraft-module**](https://www.npmjs.com/package/create-spellcraft-module)
179
158
 
180
- It can be hard to conceptualize what SpellCraft is capable of by just looking at the engine itself. Here are a set of SpellCraft modules that demonstrate the extensibility of the engine:
159
+ ## Community Modules
181
160
 
182
- |Package|Description|
161
+ | Package | Description |
183
162
  |---|---|
184
- |[**@c6fc/spellcraft-aws-auth**](https://www.npmjs.com/package/@c6fc/spellcraft-aws-auth)|Exposes the full power of the AWS SDK for JavaScript to your SpellFrames, including native support for common AWS credential sources and role-chaining.|
185
- |[**@c6fc/spellcraft-terraform**](https://www.npmjs.com/package/@c6fc/spellcraft-terraform)|Brings Terraform into your SpellCraft CLI and SpellFrames, allowing you to directly deploy the results of your Spells.|
186
- |[**@c6fc/spellcraft-packer**](https://www.npmjs.com/package/@c6fc/spellcraft-packer)|Brings Packer into SpellCraft CLI and SpellFrames, allowing you to run builds against the results of your Spells.|
187
- |[**@c6fc/spellcraft-aws-terraform**](https://www.npmjs.com/package/@c6fc/spellcraft-aws-terraform)|Contains shortcuts for common use-cases when using Terraform to deploy configurations to AWS. Including backend bootstrapping, artifact repositories, and remote state access. It also demonstrates how modules can build on the capabilities of other modules.|
188
- |[**@c6fc/spellcraft-aws-s3**](https://www.npmjs.com/package/@c6fc/spellcraft-aws-s3)|A simple but powerful module for creating AWS S3 buckets. It uses secure defaults, exposes common use-cases as optional 'types', and grants extensive control with minimal code.|
163
+ | [**@c6fc/spellcraft-aws-auth**](https://www.npmjs.com/package/@c6fc/spellcraft-aws-auth) | AWS SDK authentication and API calls directly from Jsonnet. |
164
+ | [**@c6fc/spellcraft-terraform**](https://www.npmjs.com/package/@c6fc/spellcraft-terraform) | Terraform integration and state management. |
189
165
 
166
+ ---
190
167
 
191
- ## Creating Your Own Modules
168
+ ## License
192
169
 
193
- 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)**
170
+ MIT © [Brad Woodward](https://github.com/c6fc)
package/bin/spellcraft.js CHANGED
@@ -1,67 +1,16 @@
1
1
  #! /usr/bin/env node
2
2
 
3
- /**
4
- * @fileOverview SpellCraft CLI tool.
5
- * This script provides a command-line interface for interacting with the SpellFrame
6
- * rendering engine. It allows users to generate configurations from Jsonnet files
7
- * and manage SpellCraft modules.
8
- * @module spellcraft-cli
9
- */
10
-
11
3
  'use strict';
12
4
 
13
5
  const yargs = require('yargs');
14
6
  const colors = require('@colors/colors');
15
7
  const { hideBin } = require('yargs/helpers');
16
8
  const { SpellFrame } = require('../src/index.js');
9
+ const DocGenerator = require('../src/doc-generator');
17
10
 
18
11
  const spellframe = new SpellFrame();
19
12
 
20
- // --- JSDoc Blocks for CLI Commands ---
21
- // These blocks define the commands for JSDoc.
22
- // They are not directly attached to yargs code but describe the CLI's public interface.
23
-
24
- /**
25
- * Generates files from a Jsonnet configuration.
26
- * This command processes a specified Jsonnet configuration file using the SpellFrame engine,
27
- * renders the output, and writes the resulting files to the configured directory.
28
- *
29
- * **Usage:** `spellcraft generate <filename>`
30
- *
31
- * @function generate
32
- * @name module:spellcraft-cli.generate
33
- * @param {object} argv - The arguments object provided by yargs.
34
- * @param {string} argv.filename The path to the Jsonnet configuration file to consume. (Required)
35
- *
36
- * @example
37
- * spellcraft generate ./myconfig.jsonnet
38
- */
39
-
40
- /**
41
- * Links an npm package as a SpellCraft module for the current project.
42
- * This command installs the specified npm package (if not already present) and
43
- * registers it within the project's SpellCraft module configuration, making its
44
- * functionalities available during the rendering process.
45
- *
46
- * **Usage:** `spellcraft importModule <npmPackage> [name]`
47
- *
48
- * @function importModule
49
- * @name module:spellcraft-cli.importModule
50
- * @param {object} argv - The arguments object provided by yargs.
51
- * @param {string} argv.npmPackage The NPM package name of the SpellCraft Plugin to import. (Required)
52
- * @param {string} [argv.name] An optional alias name to use for this module within SpellCraft.
53
- * If not provided, a default name from the package may be used.
54
- *
55
- * @example
56
- * spellcraft importModule my-spellcraft-enhancer
57
- * @example
58
- * spellcraft importModule @my-scope/spellcraft-utils customUtils
59
- */
60
-
61
- // --- End of JSDoc Blocks for CLI Commands ---
62
-
63
13
  (async () => {
64
- // No JSDoc for setupCli as it's an internal helper
65
14
  function setupCli(sfInstance) {
66
15
  let cli = yargs(hideBin(process.argv))
67
16
  .usage("Syntax: $0 <command> [options]")
@@ -73,6 +22,12 @@ const spellframe = new SpellFrame();
73
22
  console.log("[~] That's too arcane. (Unrecognized command)");
74
23
  })
75
24
 
25
+ .command("doc", "Generates Markdown documentation for the current module and updates README.md", () => {},
26
+ (argv) => {
27
+ const generator = new DocGenerator(process.cwd());
28
+ generator.generate();
29
+ })
30
+
76
31
  .command("generate <filename>", "Generates files from a configuration", (yargsInstance) => {
77
32
  return yargsInstance.positional('filename', {
78
33
  describe: 'Jsonnet configuration file to consume',
@@ -84,23 +39,17 @@ const spellframe = new SpellFrame();
84
39
  description: 'Leave temporary modules intact after rendering'
85
40
  });
86
41
  },
87
- async (argv) => { // No JSDoc for internal handler
88
- // try {
89
- if (argv['s']) {
90
- sfInstance.cleanModulesAfterRender = false;
91
- }
42
+ async (argv) => {
43
+ if (argv['s']) {
44
+ sfInstance.cleanModulesAfterRender = false;
45
+ }
92
46
 
93
- await sfInstance.init();
94
- await sfInstance.render(argv.filename);
95
- await sfInstance.write();
96
- console.log("[+] Generation complete.");
97
- /*} catch (error) {
98
- console.error(`[!] Error during generation: ${error.message.red}`);
99
- process.exit(1);
100
- }*/
47
+ await sfInstance.init();
48
+ await sfInstance.render(argv.filename);
49
+ await sfInstance.write();
50
+ console.log("[+] Generation complete.");
101
51
  })
102
52
 
103
- // No JSDoc for CLI extensions loop if considered internal detail
104
53
  if (sfInstance.cliExtensions && sfInstance.cliExtensions.length > 0) {
105
54
  sfInstance.cliExtensions.forEach((extensionFn) => {
106
55
  if (typeof extensionFn === 'function') {
package/lib/spellcraft CHANGED
@@ -1,6 +1,4 @@
1
1
  {
2
- local sonnetry = self,
3
-
4
2
  envvar(name):: std.native("envvar")(name),
5
3
  path():: std.native("path")()
6
4
  }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "dependencies": {
3
+ "@colors/colors": "^1.6.0",
3
4
  "@hanazuki/node-jsonnet": "^0.4.2",
4
5
  "js-yaml": "^4.1.0",
5
6
  "yargs": "^17.2.1"
6
7
  },
7
8
  "name": "@c6fc/spellcraft",
8
9
  "description": "Extensible JSonnet CLI platform",
9
- "version": "0.1.1",
10
+ "version": "0.1.3",
10
11
  "main": "src/index.js",
11
12
  "directories": {
12
13
  "lib": "lib"
@@ -16,7 +17,7 @@
16
17
  },
17
18
  "scripts": {
18
19
  "test": "node src/test.js",
19
- "doc": "jsdoc -c jsdoc.json --verbose"
20
+ "doc": "node src/doc-generator.js"
20
21
  },
21
22
  "repository": {
22
23
  "type": "git",
@@ -32,7 +33,6 @@
32
33
  },
33
34
  "homepage": "https://github.com/c6fc/spellcraft#readme",
34
35
  "devDependencies": {
35
- "clean-jsdoc-theme": "^4.3.0",
36
- "jsdoc": "^4.0.4"
36
+ "clean-jsdoc-theme": "^4.3.0"
37
37
  }
38
38
  }
@@ -0,0 +1,208 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class DocGenerator {
5
+ constructor(baseDir) {
6
+ this.baseDir = baseDir;
7
+ }
8
+
9
+ generate() {
10
+ const readmePath = path.join(this.baseDir, 'README.md');
11
+ if (!fs.existsSync(readmePath)) {
12
+ console.error("[!] No README.md found.");
13
+ return;
14
+ }
15
+
16
+ let readmeContent = fs.readFileSync(readmePath, 'utf-8');
17
+ const apiDocs = this.parseJsonnetDocs();
18
+ const cliDocs = this.parseCliDocs();
19
+
20
+ readmeContent = this.replaceSection(readmeContent, 'API', apiDocs);
21
+ readmeContent = this.replaceSection(readmeContent, 'CLI', cliDocs);
22
+
23
+ fs.writeFileSync(readmePath, readmeContent);
24
+ console.log("[+] README.md updated with generated documentation.");
25
+ }
26
+
27
+ // Helper to replace content between <!-- SPELLCRAFT_DOCS_XYZ_START --> tags
28
+ replaceSection(content, sectionName, newContent) {
29
+ const startTag = `<!-- SPELLCRAFT_DOCS_${sectionName}_START -->`;
30
+ const endTag = `<!-- SPELLCRAFT_DOCS_${sectionName}_END -->`;
31
+ const regex = new RegExp(`${startTag}[\\s\\S]*?${endTag}`, 'g');
32
+
33
+ if (!regex.test(content)) {
34
+ console.log(`[-] No tags exist for ${sectionName}. Skipping.`);
35
+ // Do nothing if the tags don't exist.
36
+ return content;
37
+ }
38
+
39
+ return content.replace(regex, `${startTag}\n${newContent}\n${endTag}`);
40
+ }
41
+
42
+ parseJsonnetDocs() {
43
+ const libPath = path.join(this.baseDir, 'module.libsonnet');
44
+ if (!fs.existsSync(libPath)) return '';
45
+
46
+ const content = fs.readFileSync(libPath, 'utf-8');
47
+ // Regex to find /** comments */ followed by a function definition
48
+ // Captures: 1=Comment content, 2=FunctionName, 3=Args
49
+ const regex = /\/\*\*([\s\S]*?)\*\/\s*\n\s*([\w]+)\(([^)]*)\)/g;
50
+
51
+ let match;
52
+ let markdown = "## API Reference\n\n";
53
+
54
+ while ((match = regex.exec(content)) !== null) {
55
+ const rawCommentLines = match[1].split('\n').map(line =>
56
+ // Remove the " * " from the start of lines
57
+ line.replace(/^\s*\*\s?/, '')
58
+ );
59
+
60
+ const funcName = match[2];
61
+ const args = match[3];
62
+
63
+ let description = [];
64
+ let params = [];
65
+ let examples = []; // Array of arrays (one per example block)
66
+ let currentExampleBlock = null;
67
+ let mode = 'description';
68
+
69
+ rawCommentLines.forEach(line => {
70
+ const trimmed = line.trim();
71
+
72
+ // 1. Detect new @example block
73
+ if (trimmed.startsWith('@example')) {
74
+ mode = 'example';
75
+ currentExampleBlock = []; // Start a new container
76
+ examples.push(currentExampleBlock);
77
+ return; // Skip the tag line itself
78
+ }
79
+
80
+ // 2. Detect Metadata tags (@param, @return)
81
+ // We handle these regardless of mode, assuming they aren't part of the code example
82
+ if (trimmed.startsWith('@param') || trimmed.startsWith('@return')) {
83
+ // Strip the @ and format as a list item
84
+ params.push(`- ${trimmed.substring(1)}`);
85
+ return;
86
+ }
87
+
88
+ // 3. Capture Content
89
+ if (mode === 'example') {
90
+ // Add line to the currently active example block
91
+ if (currentExampleBlock) {
92
+ currentExampleBlock.push(line);
93
+ }
94
+ } else {
95
+ // Add line to general description
96
+ description.push(line);
97
+ }
98
+ });
99
+
100
+ // --- Build Markdown Output ---
101
+
102
+ markdown += `### \`${funcName}(${args})\`\n\n`;
103
+
104
+ // 1. Description
105
+ if (description.length > 0) {
106
+ markdown += description.join('\n').trim() + "\n\n";
107
+ }
108
+
109
+ // 2. Parameters / Returns
110
+ if (params.length > 0) {
111
+ markdown += params.join('\n') + "\n\n";
112
+ }
113
+
114
+ // 3. Examples (Loop through the array)
115
+ if (examples.length > 0) {
116
+ markdown += "**Examples:**\n\n";
117
+ examples.forEach(exBlock => {
118
+ // Polish: Join lines and trim empty leading/trailing newlines
119
+ const code = exBlock.join('\n').trim();
120
+ if (code.length > 0) {
121
+ markdown += "```jsonnet\n";
122
+ markdown += code + "\n";
123
+ markdown += "```\n\n";
124
+ }
125
+ });
126
+ }
127
+
128
+ markdown += "---\n";
129
+ }
130
+ return markdown;
131
+ }
132
+
133
+ parseCliDocs() {
134
+ const jsPath = path.join(this.baseDir, 'module.js');
135
+ if (!fs.existsSync(jsPath)) return '';
136
+
137
+ try {
138
+ // Load the module (Bypass cache to ensure fresh read)
139
+ delete require.cache[require.resolve(jsPath)];
140
+ const moduleExports = require(jsPath);
141
+ const meta = moduleExports._spellcraft_metadata;
142
+
143
+ // Guard clauses
144
+ if (!meta || !meta.cliExtensions || typeof meta.cliExtensions !== 'function') {
145
+ return '';
146
+ }
147
+
148
+ const capturedCommands = [];
149
+
150
+ // Create a Proxy/Mock object to intercept yargs calls
151
+ const mockYargs = {
152
+ command: (command, description, ...args) => {
153
+ // Capture the essential info
154
+ capturedCommands.push({ command, description });
155
+ return mockYargs; // Return self to allow chaining .command().command()
156
+ },
157
+
158
+ // Stub out other common yargs methods so the script doesn't crash
159
+ // if the module uses .usage(), .option(), etc.
160
+ usage: () => mockYargs,
161
+ scriptName: () => mockYargs,
162
+ demandCommand: () => mockYargs,
163
+ recommendCommands: () => mockYargs,
164
+ strict: () => mockYargs,
165
+ showHelpOnFail: () => mockYargs,
166
+ help: () => mockYargs,
167
+ alias: () => mockYargs,
168
+ version: () => mockYargs,
169
+ epilogue: () => mockYargs,
170
+ option: () => mockYargs,
171
+ positional: () => mockYargs,
172
+ group: () => mockYargs,
173
+ };
174
+
175
+ // Mock SpellFrame (The second argument passed to cliExtensions)
176
+ // We mock this just in case the extension tries to read properties from it immediately
177
+ const mockSpellFrame = {
178
+ init: async () => {},
179
+ render: async () => {},
180
+ write: () => {},
181
+ };
182
+
183
+ // Execute the function!
184
+ meta.cliExtensions(mockYargs, mockSpellFrame);
185
+
186
+ // Generate Markdown
187
+ if (capturedCommands.length === 0) return '';
188
+
189
+ let markdown = "## CLI Commands\n\n";
190
+
191
+ capturedCommands.forEach(c => {
192
+ // If description is explicitly false (hidden command), skip it
193
+ if (c.description === false) return;
194
+
195
+ markdown += `- **\`spellcraft ${c.command}\`**\n`;
196
+ markdown += ` ${c.description}\n`;
197
+ });
198
+
199
+ return markdown;
200
+
201
+ } catch (e) {
202
+ console.warn(`[!] Failed to parse CLI docs from module.js: ${e.message}`);
203
+ return '';
204
+ }
205
+ }
206
+ }
207
+
208
+ module.exports = DocGenerator;
package/src/index.js CHANGED
@@ -64,7 +64,8 @@ exports.SpellFrame = class SpellFrame {
64
64
  this.jsonnet = new Jsonnet()
65
65
  .addJpath(path.join(__dirname, '../lib'))
66
66
  // REFACTOR: Look in the local project's node_modules for explicit imports
67
- .addJpath(path.join(baseDir, 'node_modules'));
67
+ .addJpath(path.join(baseDir, 'node_modules'))
68
+ .addJpath(path.join(baseDir, '.spellcraft'));
68
69
 
69
70
  // Built-in native functions
70
71
  this.addNativeFunction("envvar", (name) => process.env[name] || false, "name");
@@ -72,6 +73,9 @@ exports.SpellFrame = class SpellFrame {
72
73
 
73
74
  // REFACTOR: Automatically find and register plugins from package.json
74
75
  this.loadPluginsFromDependencies();
76
+
77
+ // 2. Load Local Magic Modules (Rapid Prototyping Mode)
78
+ this.loadLocalMagicModules();
75
79
  }
76
80
 
77
81
  _generateCacheKey(functionName, args) {
@@ -129,11 +133,6 @@ exports.SpellFrame = class SpellFrame {
129
133
  }
130
134
  }
131
135
 
132
- /**
133
- * REFACTOR: Scans the project's package.json for dependencies.
134
- * If a dependency has a 'spellcraft' key in its package.json,
135
- * load its JS entrypoint and register native functions safely.
136
- */
137
136
  loadPluginsFromDependencies() {
138
137
  const packageJsonPath = path.join(baseDir, 'package.json');
139
138
  if (!fs.existsSync(packageJsonPath)) return;
@@ -144,28 +143,104 @@ exports.SpellFrame = class SpellFrame {
144
143
  } catch (e) { return; }
145
144
 
146
145
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
146
+
147
+ // Create a require function that operates as if it's inside the user's project
148
+ const userProjectRequire = require('module').createRequire(packageJsonPath);
147
149
 
148
150
  Object.keys(deps).forEach(depName => {
149
151
  try {
150
- // Resolve the package.json of the dependency
151
- const depPath = path.dirname(require.resolve(`${depName}/package.json`, { paths: [baseDir] }));
152
- const depPkg = require(`${depName}/package.json`);
153
-
154
- // Only load if it marks itself as a spellcraft module
152
+ // 1. Find the path to the dependency's package.json using the USER'S context
153
+ const depPackageJsonPath = userProjectRequire.resolve(`${depName}/package.json`);
154
+
155
+ // 2. Load that package.json using the absolute path
156
+ const depPkg = require(depPackageJsonPath);
157
+ const depDir = path.dirname(depPackageJsonPath);
158
+
159
+ // 3. Check for SpellCraft metadata
155
160
  if (depPkg.spellcraft || depPkg.keywords?.includes("spellcraft-module")) {
156
- this.loadPlugin(depName, depPkg.main ? path.join(depPath, depPkg.main) : null);
161
+ const jsMainPath = path.join(depDir, depPkg.main || 'index.js');
162
+
163
+ // 4. Load the plugin using the calculated absolute path
164
+ this.loadPlugin(depName, jsMainPath);
157
165
  }
158
166
  } catch (e) {
159
167
  // Dependency might not be installed or resolvable, skip quietly
168
+ console.warn(`Debug: Could not load potential plugin ${depName}: ${e.message}`);
169
+ }
170
+ });
171
+ }
172
+
173
+ loadLocalMagicModules() {
174
+ const localModulesDir = path.join(baseDir, 'spellcraft_modules');
175
+ const generatedDir = path.join(baseDir, '.spellcraft');
176
+ const aggregateFile = path.join(generatedDir, 'modules');
177
+
178
+ if (!fs.existsSync(localModulesDir)) {
179
+ // Clean up if it exists so imports fail gracefully if folder is deleted
180
+ if(fs.existsSync(aggregateFile)) fs.unlinkSync(aggregateFile);
181
+ return;
182
+ }
183
+
184
+ // Ensure hidden directory exists
185
+ if (!fs.existsSync(generatedDir)) fs.mkdirSync(generatedDir, { recursive: true });
186
+
187
+ const jsFiles = fs.readdirSync(localModulesDir).filter(f => f.endsWith('.js'));
188
+
189
+ let jsonnetContentParts = [];
190
+
191
+ jsFiles.forEach(file => {
192
+ const moduleName = path.basename(file, '.js');
193
+ const fullPath = path.join(localModulesDir, file);
194
+
195
+ let moduleExports;
196
+ try {
197
+ // Cache busting for dev speed
198
+ delete require.cache[require.resolve(fullPath)];
199
+ moduleExports = require(fullPath);
200
+ } catch (e) {
201
+ console.warn(`[!] Error loading local module ${file}: ${e.message}`);
202
+ return;
203
+ }
204
+
205
+ let fileMethods = [];
206
+
207
+ Object.keys(moduleExports).forEach(funcName => {
208
+ if (funcName === '_spellcraft_metadata') return; // Skip metadata
209
+
210
+ let func, params;
211
+ // Handle [func, "arg1", "arg2"] syntax or plain function
212
+ if (Array.isArray(moduleExports[funcName])) {
213
+ [func, ...params] = moduleExports[funcName];
214
+ } else if (typeof moduleExports[funcName] === 'function') {
215
+ func = moduleExports[funcName];
216
+ // You'll need the getFunctionParameterList helper from before
217
+ params = getFunctionParameterList(func);
218
+ } else {
219
+ return;
220
+ }
221
+
222
+ // Register with a unique local prefix
223
+ const uniqueId = `local_${moduleName}_${funcName}`;
224
+ this.addNativeFunction(uniqueId, func, ...params);
225
+
226
+ // Create the Jsonnet wrapper string
227
+ // e.g. myFunc(a, b):: std.native("local_utils_myFunc")(a, b)
228
+ const paramStr = params.join(", ");
229
+ fileMethods.push(` ${funcName}(${paramStr}):: std.native("${uniqueId}")(${paramStr})`);
230
+ });
231
+
232
+ console.log(`[+] Loaded [${Object.keys(moduleExports).join(", ")}] from [${file}].`);
233
+
234
+ if (fileMethods.length > 0) {
235
+ jsonnetContentParts.push(` ${moduleName}: {\n${fileMethods.join(",\n")}\n }`);
160
236
  }
161
237
  });
238
+
239
+ // Generate the file
240
+ const finalContent = "{\n" + jsonnetContentParts.join(",\n") + "\n}";
241
+ fs.writeFileSync(aggregateFile, finalContent, 'utf-8');
162
242
  }
163
243
 
164
- /**
165
- * REFACTOR: Loads a specific plugin JS file.
166
- * Namespaces native functions using the package name to prevent collisions.
167
- * e.g., @c6fc/spellcraft-aws-auth exports 'aws' -> registered as '@c6fc/spellcraft-aws-auth:aws'
168
- */
169
244
  loadPlugin(packageName, jsMainPath) {
170
245
  if (!jsMainPath || !fs.existsSync(jsMainPath)) return;
171
246
 
@@ -238,12 +313,7 @@ exports.SpellFrame = class SpellFrame {
238
313
 
239
314
  return this.lastRender;
240
315
  }
241
-
242
- // Removed: importSpellCraftModuleFromNpm
243
- // Removed: loadModulesFromModuleDirectory
244
- // Removed: loadModulesFromPackageList
245
- // Removed: loadModuleByName (file copier)
246
-
316
+
247
317
  write(filesToWrite = this.lastRender) {
248
318
  if (!filesToWrite || typeof filesToWrite !== 'object') return this;
249
319
 
package/jsdoc.json DELETED
@@ -1,33 +0,0 @@
1
- {
2
- "tags": {
3
- "allowUnknownTags": true
4
- },
5
- "plugins": ["plugins/markdown"],
6
- "opts": {
7
- "destination": "./docs"
8
- },
9
- "source": {
10
- "include": [
11
- "src/",
12
- "bin/",
13
- "lib/"
14
- ],
15
- "includePattern": ".",
16
- "excludePattern": "\\.bak$"
17
- },
18
- "opts": {
19
- "destination": "./docs/",
20
- "encoding": "utf8",
21
- "private": true,
22
- "recurse": false,
23
- "readme": "README.md",
24
- "template": "node_modules/clean-jsdoc-theme"
25
- },
26
- "markdown": {
27
- "hardwrap": false,
28
- "idInHeadings": true
29
- },
30
- "theme_opts": {
31
- "default_theme": "dark"
32
- }
33
- }