@maestro-js/config 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @maestro-js/config
2
+
3
+ Standalone configuration loader that reads `.env` files (plaintext or AES-256-GCM encrypted), imports a TypeScript or JavaScript config module, and writes the resolved key-value pairs into `process.env`. Zero `@maestro-js` dependencies.
4
+
5
+ ## Quick Setup
6
+
7
+ ```ts
8
+ import { Config } from '@maestro-js/config'
9
+
10
+ // Load config into process.env (existing keys are NOT overwritten)
11
+ await Config.loadConfigFile({
12
+ rootDir: process.cwd(),
13
+ configName: 'app',
14
+ envName: 'production',
15
+ encryptionKey: process.env.MAESTRO_ENV_ENCRYPTION_KEY ?? null
16
+ })
17
+ ```
18
+
19
+ Or auto-load at startup without application code:
20
+
21
+ ```bash
22
+ node --import @maestro-js/config/load --config app --env production --root /path/to/project
23
+ ```
24
+
25
+ ## API Reference
26
+
27
+ ### ConfigOptions
28
+
29
+ Every primary function accepts this options object:
30
+
31
+ ```ts
32
+ type ConfigOptions = {
33
+ rootDir: string // Project root directory
34
+ configName: string // Config file name without extension (looked up in <rootDir>/config/)
35
+ envName: string | null // Environment name (.env.<envName>) or null for .env
36
+ encryptionKey: string | null // 64-char hex key for encrypted .env files, or null
37
+ }
38
+ ```
39
+
40
+ ### Config.loadConfigFile(options: ConfigOptions): Promise<void>
41
+
42
+ Read the `.env` file, import the config file, and write the resolved values into `process.env`. Keys already present in `process.env` are not overwritten.
43
+
44
+ ### Config.getConfig(options: ConfigOptions): Promise<NodeJS.Dict<string>>
45
+
46
+ Same as `loadConfigFile` but returns the resolved config object without modifying `process.env`.
47
+
48
+ ### Config.getEnvVars(options): NodeJS.Dict<string>
49
+
50
+ Read and parse an env file (plaintext or encrypted). Returns `{}` if no env file is found. Throws if an encrypted file is found but no `encryptionKey` is provided.
51
+
52
+ ```ts
53
+ Config.getEnvVars({ rootDir: '.', envName: null, encryptionKey: null })
54
+ // => { FOO: 'bar', BAZ: 'qux' }
55
+ ```
56
+
57
+ ### Config.resolveConfig(module: any): Promise<NodeJS.Dict<string>>
58
+
59
+ Normalize an imported config module into a flat string dictionary. Handles four export shapes: named exports, default object, default sync function, and default async function. Numbers and booleans are stringified. Objects and arrays throw.
60
+
61
+ ### Config.generateEncryptedEnvFile(options): void
62
+
63
+ Read a plaintext `.env` file and write its AES-256-GCM encrypted counterpart (`.env.encrypted` or `.env.<envName>.encrypted`).
64
+
65
+ ```ts
66
+ Config.generateEncryptedEnvFile({
67
+ rootDir: process.cwd(),
68
+ envName: null,
69
+ encryptionKey: key
70
+ })
71
+ ```
72
+
73
+ ### Config.generateDecryptedEnvFile(options): void
74
+
75
+ Read an encrypted `.env.encrypted` file and write the decrypted plaintext. Throws if the target file already exists unless `force: true`.
76
+
77
+ ```ts
78
+ Config.generateDecryptedEnvFile({
79
+ rootDir: process.cwd(),
80
+ envName: 'production',
81
+ encryptionKey: key,
82
+ force: true
83
+ })
84
+ ```
85
+
86
+ ### Config.generateEncryptionKey(): string
87
+
88
+ Generate a cryptographically random 32-byte key as a 64-character hex string.
89
+
90
+ ```ts
91
+ const key = Config.generateEncryptionKey()
92
+ // => '7a3f8b2c...' (64 hex chars)
93
+ ```
94
+
95
+ ### Config.Resolve<T> (type utility)
96
+
97
+ Extract the resolved key-value shape from a config module type. Use in an `environment.d.ts` file to make `process.env` type-safe.
98
+
99
+ ```ts
100
+ import type { Config } from '@maestro-js/config'
101
+ import type appConfig from './config/app.ts'
102
+
103
+ declare global {
104
+ namespace NodeJS {
105
+ interface ProcessEnv extends Config.Resolve<typeof appConfig> {}
106
+ }
107
+ }
108
+ ```
109
+
110
+ ## Common Patterns
111
+
112
+ ### Config file with named exports
113
+
114
+ Place config files in `<rootDir>/config/`. Supported extensions: `.mts`, `.cts`, `.ts`, `.mjs`, `.cjs`, `.js`.
115
+
116
+ ```ts
117
+ // config/app.ts
118
+ export const DB_HOST = 'localhost'
119
+ export const DB_PORT = '3306'
120
+ export const DEBUG = true // stringified to 'true'
121
+ ```
122
+
123
+ ### Config file as a function (derive values from .env)
124
+
125
+ When the config exports a function, it receives the merged env vars (process.env values take precedence over .env file values).
126
+
127
+ ```ts
128
+ // config/app.ts
129
+ export default (env: Record<string, string | undefined>) => ({
130
+ DATABASE_URL: `mysql://${env.DB_USER}:${env.DB_PASS}@${env.DB_HOST}:${env.DB_PORT}/${env.DB_NAME}`,
131
+ API_KEY: env.API_KEY
132
+ })
133
+ ```
134
+
135
+ ### Async config function
136
+
137
+ ```ts
138
+ // config/app.ts
139
+ export default async (env: Record<string, string | undefined>) => {
140
+ return {
141
+ SECRET: env.SECRET,
142
+ COMPUTED: await someAsyncOperation()
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### Env file resolution order
148
+
149
+ When `envName` is provided (e.g., `'production'`), files are checked in this order:
150
+ 1. `.env.production`
151
+ 2. `.env.production.encrypted`
152
+ 3. `.env`
153
+ 4. `.env.encrypted`
154
+
155
+ When `envName` is null:
156
+ 1. `.env`
157
+ 2. `.env.encrypted`
158
+
159
+ ### Encryption round-trip
160
+
161
+ ```ts
162
+ const key = Config.generateEncryptionKey()
163
+
164
+ // Encrypt: reads .env, writes .env.encrypted
165
+ Config.generateEncryptedEnvFile({ rootDir: '.', envName: null, encryptionKey: key })
166
+
167
+ // Decrypt: reads .env.encrypted, writes .env
168
+ Config.generateDecryptedEnvFile({ rootDir: '.', envName: null, encryptionKey: key, force: true })
169
+ ```
170
+
171
+ Encrypted values use the format `<iv_base64>.<authTag_base64>.<ciphertext_base64>` per key.
172
+
173
+ ### Auto-load entry point
174
+
175
+ The `@maestro-js/config/load` export is a standalone script intended for `node --import`. It parses CLI arguments and calls `Config.loadConfigFile`.
176
+
177
+ CLI flags:
178
+ - `--config <name>` (required) -- config file name without extension
179
+ - `--env <name>` -- environment name
180
+ - `--key <hex>` -- encryption key (falls back to `MAESTRO_ENV_ENCRYPTION_KEY` env var)
181
+ - `--root <path>` -- project root (defaults to `process.cwd()`)
182
+
183
+ ## Cross-Package Integration
184
+
185
+ Config is a standalone package with zero `@maestro-js` dependencies. It is used by:
186
+
187
+ - **CLI** -- The Maestro CLI uses `Config.generateEncryptedEnvFile`, `Config.generateDecryptedEnvFile`, and `Config.generateEncryptionKey` for the `env:encrypt`, `env:decrypt`, and `env:gen-key` commands.
188
+ - **Application boot** -- Applications use `Config.loadConfigFile` (or `node --import @maestro-js/config/load`) to populate `process.env` before initializing other Maestro packages that read from `process.env`.
189
+ - **Type safety** -- The `Config.Resolve` utility type lets applications create a typed `process.env` derived from the config module shape.
190
+
191
+ ## Testing
192
+
193
+ ```bash
194
+ pnpm --filter @maestro-js/config test
195
+ cd packages/config && npx beartest ./tests/config.test.ts
196
+ ```
197
+
198
+ Tests use `beartest-js` with `expect`. They create temporary directories, write config/env files, and verify loading, encryption round-trips, and error cases.
@@ -0,0 +1,187 @@
1
+ // src/index.ts
2
+ import "async_hooks";
3
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
4
+ import { existsSync, readFileSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ import { parseEnv } from "util";
7
+ async function getConfig({ rootDir, configName, envName, encryptionKey }) {
8
+ const env = getEnvVars({ rootDir, envName, encryptionKey });
9
+ const configFilePath = getConfigFilePath({ rootDir, configName });
10
+ if (!configFilePath) {
11
+ return env;
12
+ }
13
+ const config = await resolveConfig(import(configFilePath), env);
14
+ return config;
15
+ }
16
+ async function loadConfigFile(options) {
17
+ const config = await getConfig(options);
18
+ for (const [key, value] of Object.entries(config)) {
19
+ if (process.env[key] === void 0) {
20
+ process.env[key] = value;
21
+ }
22
+ }
23
+ }
24
+ function resolveEnvValues(dotEnvFileValues) {
25
+ const processKeys = Object.keys(process.env);
26
+ const envFileKeys = Object.keys(dotEnvFileValues);
27
+ return Object.fromEntries(
28
+ [.../* @__PURE__ */ new Set([...processKeys, ...envFileKeys])].map((key) => [key, process.env[key] ?? dotEnvFileValues[key]])
29
+ );
30
+ }
31
+ async function resolveConfig(module, env) {
32
+ let config = await module;
33
+ if (config && typeof config === "object" && "default" in config) {
34
+ config = config.default;
35
+ }
36
+ const raw = typeof config === "function" ? await config(resolveEnvValues(env)) : config;
37
+ const result = {};
38
+ for (const [key, value] of Object.entries(raw)) {
39
+ if (typeof value === "string") {
40
+ result[key] = value;
41
+ } else if (typeof value === "number" || typeof value === "boolean") {
42
+ result[key] = String(value);
43
+ } else if (value != null) {
44
+ throw new Error(
45
+ `Config key "${key}" has unsupported type ${typeof value}. Only string, number, and boolean values are allowed.`
46
+ );
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+ var CONFIG_EXTENSIONS = [".mts", ".cts", ".ts", ".mjs", ".cjs", ".js"];
52
+ function getConfigFilePath({ rootDir, configName }) {
53
+ for (const ext of CONFIG_EXTENSIONS) {
54
+ const filePath = join(rootDir, "config", `${configName}${ext}`);
55
+ if (existsSync(filePath)) {
56
+ return filePath;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ function getEnvVars({
62
+ rootDir,
63
+ envName,
64
+ encryptionKey
65
+ }) {
66
+ const envFile = getEnvFilePath({ rootDir, envName });
67
+ if (!envFile) {
68
+ return {};
69
+ }
70
+ const envFileContents = readFileSync(envFile, "utf-8");
71
+ const isEncrypted = envFile.endsWith(".encrypted");
72
+ if (isEncrypted) {
73
+ if (!encryptionKey) {
74
+ throw new Error(`Cannot decrypt ${envFile}. Provide an encryption key via --key or MAESTRO_ENV_ENCRYPTION_KEY`);
75
+ } else {
76
+ const decrypted = decryptEnvFileContents(envFileContents, encryptionKey);
77
+ return decrypted;
78
+ }
79
+ } else {
80
+ const parsed = parseEnv(envFileContents);
81
+ return parsed;
82
+ }
83
+ }
84
+ function generateEncryptedEnvFile({
85
+ rootDir,
86
+ envName: envNameParam,
87
+ encryptionKey
88
+ }) {
89
+ const envName = envNameParam || process.env.ENV_NAME || null;
90
+ const sourcePath = join(rootDir, envFileName(envName));
91
+ const destPath = join(rootDir, encryptedEnvFileName(envName));
92
+ const contents = readFileSync(sourcePath, "utf-8");
93
+ parseEnv(contents);
94
+ const encrypted = encryptEnvFileContents(contents, encryptionKey);
95
+ writeFileSync(destPath, envRecordToFileContents(encrypted));
96
+ }
97
+ function generateDecryptedEnvFile({
98
+ rootDir,
99
+ envName: envNameParam,
100
+ encryptionKey,
101
+ force
102
+ }) {
103
+ const envName = envNameParam || process.env.ENV_NAME || null;
104
+ const sourcePath = join(rootDir, encryptedEnvFileName(envName));
105
+ const destPath = join(rootDir, envFileName(envName));
106
+ if (!force && existsSync(destPath)) {
107
+ throw new Error(`${destPath} already exists. Use force to overwrite.`);
108
+ }
109
+ const contents = readFileSync(sourcePath, "utf-8");
110
+ const decrypted = decryptEnvFileContents(contents, encryptionKey);
111
+ writeFileSync(destPath, envRecordToFileContents(decrypted));
112
+ }
113
+ function generateEncryptionKey() {
114
+ return randomBytes(32).toString("hex");
115
+ }
116
+ function envFileName(envName) {
117
+ return envName ? `.env.${envName}` : ".env";
118
+ }
119
+ function encryptedEnvFileName(envName) {
120
+ return `${envFileName(envName)}.encrypted`;
121
+ }
122
+ function envRecordToFileContents(env) {
123
+ return Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n");
124
+ }
125
+ function validateEncryptionKey(encryptionKey) {
126
+ const keyBuffer = Buffer.from(encryptionKey, "hex");
127
+ if (keyBuffer.length !== 32) {
128
+ throw new Error(`Encryption key must be 32 bytes (64 hex characters), got ${keyBuffer.length} bytes`);
129
+ }
130
+ return keyBuffer;
131
+ }
132
+ function encryptEnvFileContents(envFileContents, encryptionKey) {
133
+ const env = parseEnv(envFileContents);
134
+ const keyBuffer = validateEncryptionKey(encryptionKey);
135
+ const result = {};
136
+ for (const [name, value] of Object.entries(env)) {
137
+ const iv = randomBytes(12);
138
+ const cipher = createCipheriv("aes-256-gcm", keyBuffer, iv);
139
+ const ciphertext = Buffer.concat([cipher.update(value, "utf-8"), cipher.final()]);
140
+ const authTag = cipher.getAuthTag();
141
+ result[name] = `${iv.toString("base64")}.${authTag.toString("base64")}.${ciphertext.toString("base64")}`;
142
+ }
143
+ return result;
144
+ }
145
+ function decryptEnvFileContents(envFileContents, encryptionKey) {
146
+ const env = parseEnv(envFileContents);
147
+ const keyBuffer = validateEncryptionKey(encryptionKey);
148
+ const result = {};
149
+ for (const [name, value] of Object.entries(env)) {
150
+ const parts = value.split(".");
151
+ if (parts.length !== 3) {
152
+ throw new Error(`Invalid encrypted value format for key "${name}"`);
153
+ }
154
+ const [ivB64, authTagB64, ciphertextB64] = parts;
155
+ const iv = Buffer.from(ivB64, "base64");
156
+ const authTag = Buffer.from(authTagB64, "base64");
157
+ const ciphertext = Buffer.from(ciphertextB64, "base64");
158
+ const decipher = createDecipheriv("aes-256-gcm", keyBuffer, iv);
159
+ decipher.setAuthTag(authTag);
160
+ result[name] = decipher.update(ciphertext).toString("utf-8") + decipher.final("utf-8");
161
+ }
162
+ return result;
163
+ }
164
+ function getEnvFilePath({ rootDir, envName: envNameParam }) {
165
+ const envName = envNameParam || process.env.ENV_NAME;
166
+ const candidates = envName ? [envFileName(envName), encryptedEnvFileName(envName), envFileName(null), encryptedEnvFileName(null)] : [envFileName(null), encryptedEnvFileName(null)];
167
+ for (const fileName of candidates) {
168
+ const filePath = join(rootDir, fileName);
169
+ if (existsSync(filePath)) {
170
+ return filePath;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+ var Config = {
176
+ getConfig,
177
+ resolveConfig,
178
+ loadConfigFile,
179
+ getEnvVars,
180
+ generateEncryptedEnvFile,
181
+ generateDecryptedEnvFile,
182
+ generateEncryptionKey
183
+ };
184
+
185
+ export {
186
+ Config
187
+ };
@@ -0,0 +1,87 @@
1
+ type ConfigOptions = {
2
+ rootDir: string;
3
+ configName: string;
4
+ envName: string | null;
5
+ encryptionKey: string | null;
6
+ };
7
+ /** Reads the `.env` file, imports the config file, and returns the resolved config. Does not modify `process.env`. */
8
+ declare function getConfig({ rootDir, configName, envName, encryptionKey }: ConfigOptions): Promise<NodeJS.Dict<string>>;
9
+ /** Reads the `.env` file, imports the config file, and writes the result into `process.env`. Keys already present are not overwritten. */
10
+ declare function loadConfigFile(options: ConfigOptions): Promise<void>;
11
+ /** Normalizes an imported config module into a flat string dictionary. Handles default exports, named exports, sync/async functions. */
12
+ declare function resolveConfig(module: any, env: NodeJS.Dict<string>): Promise<NodeJS.Dict<string>>;
13
+ /** Reads and parses an env file (plaintext or encrypted). Returns `{}` if no env file is found. */
14
+ declare function getEnvVars({ rootDir, envName, encryptionKey }: {
15
+ rootDir: string;
16
+ envName: string | null;
17
+ encryptionKey: string | null;
18
+ }): NodeJS.Dict<string>;
19
+ /** Reads a plaintext `.env` file and writes its AES-256-GCM encrypted counterpart. */
20
+ declare function generateEncryptedEnvFile({ rootDir, envName: envNameParam, encryptionKey }: {
21
+ rootDir: string;
22
+ envName: string | null;
23
+ encryptionKey: string;
24
+ }): void;
25
+ /** Reads an encrypted `.env.encrypted` file and writes the decrypted plaintext. Throws if the target exists unless `force` is true. */
26
+ declare function generateDecryptedEnvFile({ rootDir, envName: envNameParam, encryptionKey, force }: {
27
+ rootDir: string;
28
+ envName: string | null;
29
+ encryptionKey: string;
30
+ force?: boolean;
31
+ }): void;
32
+ /** Generates a cryptographically random 32-byte key as a 64-character hex string. */
33
+ declare function generateEncryptionKey(): string;
34
+ type ConfigValue = Record<string, string | undefined>;
35
+ type Unwrap<T> = T extends (...any: any[]) => Promise<infer R> ? R : T extends (...any: any[]) => infer R ? R : T;
36
+ declare const Config: {
37
+ getConfig: typeof getConfig;
38
+ resolveConfig: typeof resolveConfig;
39
+ loadConfigFile: typeof loadConfigFile;
40
+ getEnvVars: typeof getEnvVars;
41
+ generateEncryptedEnvFile: typeof generateEncryptedEnvFile;
42
+ generateDecryptedEnvFile: typeof generateDecryptedEnvFile;
43
+ generateEncryptionKey: typeof generateEncryptionKey;
44
+ };
45
+ /**
46
+ * Configuration loader for Node.js applications.
47
+ *
48
+ * Reads `.env` files (plaintext or AES-256-GCM encrypted) and imports a TypeScript or JavaScript
49
+ * config file whose exported values are written to `process.env`. When the config file exports a
50
+ * function, it receives the parsed `.env` vars as its argument, so you can derive computed values
51
+ * (e.g., building a database URL from individual host/port/user parts). Existing `process.env`
52
+ * keys take precedence and are not overwritten.
53
+ *
54
+ * Config has zero `@maestro-js` dependencies. It is a standalone utility used by the CLI for
55
+ * `env:encrypt`, `env:decrypt`, and `env:gen-key` commands, and by applications at boot time
56
+ * via `node --import @maestro-js/config/load`.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { Config } from '@maestro-js/config'
61
+ *
62
+ * // Load config into process.env
63
+ * await Config.loadConfigFile({
64
+ * rootDir: process.cwd(),
65
+ * configName: 'app',
66
+ * envName: 'production',
67
+ * encryptionKey: process.env.MAESTRO_ENV_ENCRYPTION_KEY ?? null
68
+ * })
69
+ *
70
+ * // Or get config without modifying process.env
71
+ * const config = await Config.getConfig({ rootDir: '.', configName: 'app', envName: null, encryptionKey: null })
72
+ * ```
73
+ */
74
+ declare namespace Config {
75
+ /**
76
+ * Extracts the resolved key-value shape from a config module type. Handles all four export shapes:
77
+ * named exports, default object, default sync function, and default async function. Use it in an
78
+ * `environment.d.ts` file to make `process.env` type-safe.
79
+ */
80
+ type Resolve<T extends ConfigValue | ((...any: any[]) => ConfigValue) | ((...any: any[]) => Promise<ConfigValue>) | {
81
+ default: ConfigValue | ((...any: any[]) => ConfigValue) | ((...any: any[]) => Promise<ConfigValue>);
82
+ }> = T extends {
83
+ default: infer D;
84
+ } ? Unwrap<D> : Unwrap<T>;
85
+ }
86
+
87
+ export { Config };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ Config
3
+ } from "./chunk-AVYD4MV2.js";
4
+ export {
5
+ Config
6
+ };
package/dist/load.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/load.js ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ Config
3
+ } from "./chunk-AVYD4MV2.js";
4
+
5
+ // src/load.ts
6
+ import "path";
7
+ var { configName, envName, encryptionKey, rootDir } = parseArgs(process.argv);
8
+ await Config.loadConfigFile({ rootDir, configName, envName, encryptionKey });
9
+ function parseArgs(argv) {
10
+ const configName2 = getArgValue(argv, "--config");
11
+ if (!configName2) {
12
+ throw new Error("Missing required --config <name> argument");
13
+ }
14
+ const envName2 = getArgValue(argv, "--env") ?? null;
15
+ const encryptionKey2 = getArgValue(argv, "--key") ?? process.env["MAESTRO_ENV_ENCRYPTION_KEY"] ?? null;
16
+ const rootDir2 = getArgValue(argv, "--root") ?? process.cwd();
17
+ return { configName: configName2, envName: envName2, encryptionKey: encryptionKey2, rootDir: rootDir2 };
18
+ }
19
+ function getArgValue(argv, flag) {
20
+ const index = argv.indexOf(flag);
21
+ if (index === -1 || index + 1 >= argv.length) return void 0;
22
+ return argv[index + 1];
23
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@maestro-js/config",
3
+ "description": "Configuration loader for .env files and TypeScript/JavaScript config modules with AES-256-GCM encryption support. Use when working with @maestro-js/config, including loading config into process.env, reading/writing encrypted .env files, creating config modules, deriving computed env values, and generating encryption keys. Key capabilities: load config files, parse .env files (plaintext or encrypted), encrypt/decrypt env files, generate encryption keys, resolve config modules (named exports, default exports, sync/async functions), type-safe process.env via Config.Resolve utility type, and auto-load via node --import @maestro-js/config/load.",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ },
10
+ "./load": {
11
+ "types": "./dist/load.d.ts",
12
+ "default": "./dist/load.js"
13
+ }
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.19.11"
17
+ },
18
+ "version": "1.0.0-alpha.0",
19
+ "publishConfig": {
20
+ "access": "restricted"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "license": "UNLICENSED",
26
+ "engines": {
27
+ "node": ">=22.18.0"
28
+ },
29
+ "repository": "https://github.com/Marcato-Partners/maestro-js",
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "watch": "tsup --watch",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "beartest ./tests/**/*",
35
+ "format": "prettier --write src/ tests/",
36
+ "lint": "prettier --check src/ tests/"
37
+ }
38
+ }