zayra-core 0.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/README.md +148 -0
- package/package.json +30 -0
- package/src/config.js +198 -0
- package/src/core.js +286 -0
- package/src/errors.js +72 -0
- package/src/index.js +19 -0
- package/src/logger.js +92 -0
- package/src/utils.js +175 -0
- package/src/version.js +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# zayra-core
|
|
2
|
+
|
|
3
|
+
The core foundation package of **ZAYRA AI**. It manages the core lifecycle and architecture of ZAYRA — initialization, module loading, configuration, logging, and error handling.
|
|
4
|
+
|
|
5
|
+
This is version `0.0.1`: the first development version. It contains **no frontend/UI code** — only core architecture. Future ZAYRA packages (`zayra-config`, `zayra-memory`, `zayra-train`, `zayra-ai-router`, `zayra-provider`, `zayra-tools`, `zayra-sdk`) will depend on this package.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install zayra-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { Zayra } from "zayra-core";
|
|
17
|
+
|
|
18
|
+
const zayra = new Zayra();
|
|
19
|
+
|
|
20
|
+
await zayra.initialize();
|
|
21
|
+
await zayra.start();
|
|
22
|
+
|
|
23
|
+
console.log(zayra.status());
|
|
24
|
+
// { state: "running", version: "0.0.1", modules: [], uptimeMs: ... }
|
|
25
|
+
|
|
26
|
+
await zayra.stop();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Registering modules
|
|
30
|
+
|
|
31
|
+
Future ZAYRA packages plug into core via `.use(name, moduleInstance)`. A module is any object that optionally implements `initialize()`, `start()`, and/or `stop()`:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { Zayra } from "zayra-core";
|
|
35
|
+
|
|
36
|
+
const memoryModule = {
|
|
37
|
+
async initialize({ config, logger }) {
|
|
38
|
+
logger.info("memory module initializing");
|
|
39
|
+
},
|
|
40
|
+
async start() {
|
|
41
|
+
/* ... */
|
|
42
|
+
},
|
|
43
|
+
async stop() {
|
|
44
|
+
/* ... */
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const zayra = new Zayra();
|
|
49
|
+
zayra.use("memory", memoryModule);
|
|
50
|
+
|
|
51
|
+
await zayra.start(); // also runs initialize() automatically if needed
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
zayra-core loads configuration from, in increasing priority:
|
|
57
|
+
|
|
58
|
+
1. `config.json` in the working directory (optional)
|
|
59
|
+
2. `.env` in the working directory (optional)
|
|
60
|
+
3. real `process.env` variables
|
|
61
|
+
|
|
62
|
+
**zayra-core never hardcodes API keys, secrets, or private data.** All sensitive values must be provided via `.env` or your real environment.
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { loadConfig, requireConfigValue } from "zayra-core";
|
|
66
|
+
|
|
67
|
+
const config = await loadConfig();
|
|
68
|
+
const apiKey = requireConfigValue(config, "ZAYRA_API_KEY"); // throws ConfigError if missing
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
See `.env.example` for the shape of a `.env` file.
|
|
72
|
+
|
|
73
|
+
## Logging
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { logger, Logger } from "zayra-core";
|
|
77
|
+
|
|
78
|
+
logger.info("hello");
|
|
79
|
+
logger.warn("careful");
|
|
80
|
+
logger.error("something broke");
|
|
81
|
+
|
|
82
|
+
// Scoped logger for a specific module
|
|
83
|
+
const memoryLogger = new Logger({ scope: "memory" });
|
|
84
|
+
memoryLogger.info("memory module ready");
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Errors
|
|
88
|
+
|
|
89
|
+
zayra-core never fails silently. All internal errors are instances of one of:
|
|
90
|
+
|
|
91
|
+
- `ZayraError` — base error class
|
|
92
|
+
- `ConfigError` — configuration loading/validation failures
|
|
93
|
+
- `ModuleError` — module registration/lifecycle failures
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { ConfigError } from "zayra-core";
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await zayra.initialize();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err instanceof ConfigError) {
|
|
102
|
+
console.error("Bad config:", err.message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Project structure
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
zayra-core/
|
|
111
|
+
src/
|
|
112
|
+
index.js # public entry point
|
|
113
|
+
core.js # Zayra class
|
|
114
|
+
config.js # config.json / .env / process.env loader
|
|
115
|
+
logger.js # Logger class + default logger
|
|
116
|
+
errors.js # ZayraError, ConfigError, ModuleError
|
|
117
|
+
utils.js # file, validation, formatting, path helpers
|
|
118
|
+
version.js # package version constant
|
|
119
|
+
package.json
|
|
120
|
+
README.md
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## API reference
|
|
124
|
+
|
|
125
|
+
### `class Zayra`
|
|
126
|
+
|
|
127
|
+
| Method | Description |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `new Zayra(options?)` | Creates a new instance. `options.logger`, `options.cwd`, `options.autoLoadConfig`. |
|
|
130
|
+
| `initialize()` | Loads config and runs each module's `initialize()` hook. |
|
|
131
|
+
| `start()` | Runs `initialize()` if needed, then runs each module's `start()` hook. |
|
|
132
|
+
| `stop()` | Runs each module's `stop()` hook. |
|
|
133
|
+
| `status()` | Returns `{ state, version, modules, uptimeMs }`. |
|
|
134
|
+
| `use(name, moduleInstance)` | Registers a module. Returns `this` for chaining. |
|
|
135
|
+
| `getModule(name)` | Retrieves a registered module instance. |
|
|
136
|
+
| `listModules()` | Returns an array of registered module names. |
|
|
137
|
+
|
|
138
|
+
### Config helpers
|
|
139
|
+
|
|
140
|
+
`loadConfig(options?)`, `requireConfigValue(config, key)`, `getConfigValue(config, key, defaultValue?)`
|
|
141
|
+
|
|
142
|
+
### Utilities
|
|
143
|
+
|
|
144
|
+
`utils.fileExists`, `utils.readJsonFile`, `utils.writeJsonFile`, `utils.isPlainObject`, `utils.isNonEmptyString`, `utils.assert`, `utils.formatTimestamp`, `utils.safeStringify`, `utils.dirnameFromMeta`, `utils.resolvePath`
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zayra-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Core foundation package for ZAYRA AI — manages the core lifecycle and architecture that future zayra-* packages build on.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test \"test/**/*.test.js\""
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"zayra",
|
|
22
|
+
"ai",
|
|
23
|
+
"core",
|
|
24
|
+
"foundation",
|
|
25
|
+
"lifecycle"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {}
|
|
30
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.js
|
|
3
|
+
*
|
|
4
|
+
* Configuration system for ZAYRA core.
|
|
5
|
+
*
|
|
6
|
+
* Supports two sources, merged in this order (later overrides earlier):
|
|
7
|
+
* 1. config.json (project defaults, optional, checked into repo)
|
|
8
|
+
* 2. .env (environment-specific values, NOT checked into repo)
|
|
9
|
+
* 3. process.env (real environment variables, e.g. from the shell / CI)
|
|
10
|
+
*
|
|
11
|
+
* This package never hardcodes API keys, secrets, or private data.
|
|
12
|
+
* Those must always come from the environment.
|
|
13
|
+
*
|
|
14
|
+
* Note: this is a deliberately small, dependency-free .env parser
|
|
15
|
+
* (no `dotenv` package) to keep zayra-core's dependency footprint at
|
|
16
|
+
* zero for 0.0.1. It covers the common KEY=VALUE cases; if more
|
|
17
|
+
* advanced .env features are needed later (multiline values, etc.),
|
|
18
|
+
* this is the place to extend, or swap in `dotenv` behind the same
|
|
19
|
+
* loadConfig() API.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { promises as fs } from "node:fs";
|
|
24
|
+
import { ConfigError } from "./errors.js";
|
|
25
|
+
import { fileExists, isPlainObject, readJsonFile } from "./utils.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses raw .env file content into a plain object.
|
|
29
|
+
* Supports `KEY=VALUE`, blank lines, `#` comments, and quoted values.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} content
|
|
32
|
+
* @returns {Record<string, string>}
|
|
33
|
+
*/
|
|
34
|
+
function parseEnvContent(content) {
|
|
35
|
+
const result = {};
|
|
36
|
+
|
|
37
|
+
const lines = content.split(/\r?\n/);
|
|
38
|
+
|
|
39
|
+
for (const rawLine of lines) {
|
|
40
|
+
const line = rawLine.trim();
|
|
41
|
+
|
|
42
|
+
if (!line || line.startsWith("#")) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const separatorIndex = line.indexOf("=");
|
|
47
|
+
if (separatorIndex === -1) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
52
|
+
let value = line.slice(separatorIndex + 1).trim();
|
|
53
|
+
|
|
54
|
+
if (!key) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Strip matching surrounding quotes, if present.
|
|
59
|
+
const isQuoted =
|
|
60
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
61
|
+
(value.startsWith("'") && value.endsWith("'"));
|
|
62
|
+
|
|
63
|
+
if (isQuoted) {
|
|
64
|
+
value = value.slice(1, -1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
result[key] = value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Loads and parses a .env file, if it exists.
|
|
75
|
+
* Returns an empty object if the file is missing (this is not an error —
|
|
76
|
+
* .env is optional, especially in production where real env vars are used).
|
|
77
|
+
*
|
|
78
|
+
* @param {string} envPath
|
|
79
|
+
* @returns {Promise<Record<string, string>>}
|
|
80
|
+
*/
|
|
81
|
+
async function loadDotEnv(envPath) {
|
|
82
|
+
const exists = await fileExists(envPath);
|
|
83
|
+
if (!exists) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const content = await fs.readFile(envPath, "utf-8");
|
|
89
|
+
return parseEnvContent(content);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new ConfigError(`Failed to read .env file at "${envPath}": ${err.message}`, {
|
|
92
|
+
cause: err,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Loads and parses a config.json file, if it exists.
|
|
99
|
+
* Returns an empty object if the file is missing.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} configPath
|
|
102
|
+
* @returns {Promise<Record<string, any>>}
|
|
103
|
+
*/
|
|
104
|
+
async function loadConfigJson(configPath) {
|
|
105
|
+
const exists = await fileExists(configPath);
|
|
106
|
+
if (!exists) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = await readJsonFile(configPath);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw new ConfigError(`Failed to parse config.json at "${configPath}": ${err.message}`, {
|
|
115
|
+
cause: err,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isPlainObject(parsed)) {
|
|
120
|
+
throw new ConfigError(`config.json at "${configPath}" must contain a JSON object at the top level.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Loads ZAYRA configuration by merging config.json, .env, and
|
|
128
|
+
* process.env, in that priority order (process.env wins).
|
|
129
|
+
*
|
|
130
|
+
* @param {object} [options]
|
|
131
|
+
* @param {string} [options.cwd] - Base directory to resolve config.json/.env from. Defaults to process.cwd().
|
|
132
|
+
* @param {string} [options.configFileName="config.json"]
|
|
133
|
+
* @param {string} [options.envFileName=".env"]
|
|
134
|
+
* @param {boolean} [options.includeProcessEnv=true] - Whether to merge in real process.env values.
|
|
135
|
+
* @returns {Promise<Record<string, any>>} Merged configuration object.
|
|
136
|
+
*/
|
|
137
|
+
export async function loadConfig(options = {}) {
|
|
138
|
+
const cwd = options.cwd ?? process.cwd();
|
|
139
|
+
const configFileName = options.configFileName ?? "config.json";
|
|
140
|
+
const envFileName = options.envFileName ?? ".env";
|
|
141
|
+
const includeProcessEnv = options.includeProcessEnv ?? true;
|
|
142
|
+
|
|
143
|
+
const configPath = path.resolve(cwd, configFileName);
|
|
144
|
+
const envPath = path.resolve(cwd, envFileName);
|
|
145
|
+
|
|
146
|
+
const [fileConfig, dotEnvConfig] = await Promise.all([
|
|
147
|
+
loadConfigJson(configPath),
|
|
148
|
+
loadDotEnv(envPath),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const merged = {
|
|
152
|
+
...fileConfig,
|
|
153
|
+
...dotEnvConfig,
|
|
154
|
+
...(includeProcessEnv ? process.env : {}),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return merged;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Retrieves a single required config value, throwing a ConfigError
|
|
162
|
+
* if it is missing or empty. Use this for values that ZAYRA cannot
|
|
163
|
+
* function without (e.g. a required API key name, not its value
|
|
164
|
+
* hardcoded — the value itself always comes from the environment).
|
|
165
|
+
*
|
|
166
|
+
* @param {Record<string, any>} config - A config object, typically from loadConfig().
|
|
167
|
+
* @param {string} key
|
|
168
|
+
* @returns {any}
|
|
169
|
+
* @throws {ConfigError}
|
|
170
|
+
*/
|
|
171
|
+
export function requireConfigValue(config, key) {
|
|
172
|
+
const value = config?.[key];
|
|
173
|
+
|
|
174
|
+
if (value === undefined || value === null || value === "") {
|
|
175
|
+
throw new ConfigError(`Missing required configuration value: "${key}". Set it via .env or environment variables.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Retrieves a single optional config value, falling back to a default.
|
|
183
|
+
*
|
|
184
|
+
* @param {Record<string, any>} config
|
|
185
|
+
* @param {string} key
|
|
186
|
+
* @param {any} [defaultValue]
|
|
187
|
+
* @returns {any}
|
|
188
|
+
*/
|
|
189
|
+
export function getConfigValue(config, key, defaultValue = undefined) {
|
|
190
|
+
const value = config?.[key];
|
|
191
|
+
return value === undefined ? defaultValue : value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default {
|
|
195
|
+
loadConfig,
|
|
196
|
+
requireConfigValue,
|
|
197
|
+
getConfigValue,
|
|
198
|
+
};
|
package/src/core.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core.js
|
|
3
|
+
*
|
|
4
|
+
* Defines the main Zayra class — the central orchestrator of the
|
|
5
|
+
* ZAYRA AI system. This class is responsible for:
|
|
6
|
+
* - initializing the system
|
|
7
|
+
* - loading and managing modules (future packages: zayra-config,
|
|
8
|
+
* zayra-memory, zayra-train, zayra-ai-router, zayra-provider,
|
|
9
|
+
* zayra-tools, zayra-sdk)
|
|
10
|
+
* - managing the system lifecycle (initialize -> start -> stop)
|
|
11
|
+
* - providing a simple status snapshot
|
|
12
|
+
*
|
|
13
|
+
* Modules are kept independent: Zayra only knows about the generic
|
|
14
|
+
* "module" shape (an object with optional initialize/start/stop
|
|
15
|
+
* methods), never about the internals of any specific package. This
|
|
16
|
+
* keeps zayra-core decoupled from packages that don't exist yet.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { logger as defaultLogger } from "./logger.js";
|
|
20
|
+
import { loadConfig } from "./config.js";
|
|
21
|
+
import { ZayraError, ModuleError } from "./errors.js";
|
|
22
|
+
import { VERSION } from "./version.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Lifecycle states for the Zayra core engine.
|
|
26
|
+
* @readonly
|
|
27
|
+
* @enum {string}
|
|
28
|
+
*/
|
|
29
|
+
export const ZayraState = Object.freeze({
|
|
30
|
+
CREATED: "created",
|
|
31
|
+
INITIALIZING: "initializing",
|
|
32
|
+
INITIALIZED: "initialized",
|
|
33
|
+
STARTING: "starting",
|
|
34
|
+
RUNNING: "running",
|
|
35
|
+
STOPPING: "stopping",
|
|
36
|
+
STOPPED: "stopped",
|
|
37
|
+
ERRORED: "errored",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Main ZAYRA core engine.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* import { Zayra } from "zayra-core";
|
|
45
|
+
*
|
|
46
|
+
* const ai = new Zayra();
|
|
47
|
+
* await ai.initialize();
|
|
48
|
+
* await ai.start();
|
|
49
|
+
* console.log(ai.status());
|
|
50
|
+
* await ai.stop();
|
|
51
|
+
*/
|
|
52
|
+
export class Zayra {
|
|
53
|
+
/**
|
|
54
|
+
* @param {object} [options]
|
|
55
|
+
* @param {import("./logger.js").Logger} [options.logger] - Custom logger instance. Defaults to the shared zayra-core logger.
|
|
56
|
+
* @param {string} [options.cwd] - Working directory used to resolve config.json/.env. Defaults to process.cwd().
|
|
57
|
+
* @param {boolean} [options.autoLoadConfig=true] - Whether initialize() should automatically load config.json/.env/process.env.
|
|
58
|
+
*/
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
this.logger = options.logger ?? defaultLogger;
|
|
61
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
62
|
+
this.autoLoadConfig = options.autoLoadConfig ?? true;
|
|
63
|
+
|
|
64
|
+
this.version = VERSION;
|
|
65
|
+
this.state = ZayraState.CREATED;
|
|
66
|
+
this.config = {};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Registered modules, keyed by name.
|
|
70
|
+
* Each entry: { name, instance }
|
|
71
|
+
* @type {Map<string, { name: string, instance: any }>}
|
|
72
|
+
*/
|
|
73
|
+
this._modules = new Map();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ------------------------------------------------------------------ */
|
|
77
|
+
/* Module architecture */
|
|
78
|
+
/* ------------------------------------------------------------------ */
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Registers a module with the core engine. Modules are how future
|
|
82
|
+
* packages (zayra-config, zayra-memory, zayra-train, zayra-ai-router,
|
|
83
|
+
* zayra-provider, zayra-tools, zayra-sdk) plug into ZAYRA, without
|
|
84
|
+
* core needing to know their internals.
|
|
85
|
+
*
|
|
86
|
+
* A module is any object, optionally implementing:
|
|
87
|
+
* - async initialize(context) - called when core.initialize() runs
|
|
88
|
+
* - async start(context) - called when core.start() runs
|
|
89
|
+
* - async stop(context) - called when core.stop() runs
|
|
90
|
+
*
|
|
91
|
+
* @param {string} name - Unique module name (e.g. "memory", "router").
|
|
92
|
+
* @param {object} moduleInstance - The module implementation.
|
|
93
|
+
* @returns {this} For chaining, e.g. zayra.use("memory", memoryModule).use(...)
|
|
94
|
+
* @throws {ModuleError} If a module with this name is already registered.
|
|
95
|
+
*/
|
|
96
|
+
use(name, moduleInstance) {
|
|
97
|
+
if (this._modules.has(name)) {
|
|
98
|
+
throw new ModuleError(`Module "${name}" is already registered.`, { moduleName: name });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this._modules.set(name, { name, instance: moduleInstance });
|
|
102
|
+
this.logger.info(`Module registered: ${name}`);
|
|
103
|
+
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Retrieves a previously registered module by name.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} name
|
|
111
|
+
* @returns {any} The module instance.
|
|
112
|
+
* @throws {ModuleError} If no module with this name is registered.
|
|
113
|
+
*/
|
|
114
|
+
getModule(name) {
|
|
115
|
+
const entry = this._modules.get(name);
|
|
116
|
+
if (!entry) {
|
|
117
|
+
throw new ModuleError(`Module "${name}" is not registered.`, { moduleName: name });
|
|
118
|
+
}
|
|
119
|
+
return entry.instance;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns the names of all currently registered modules.
|
|
124
|
+
* @returns {string[]}
|
|
125
|
+
*/
|
|
126
|
+
listModules() {
|
|
127
|
+
return Array.from(this._modules.keys());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Calls a given lifecycle hook (e.g. "initialize", "start", "stop")
|
|
132
|
+
* on every registered module that implements it, in registration order.
|
|
133
|
+
*
|
|
134
|
+
* @param {"initialize"|"start"|"stop"} hookName
|
|
135
|
+
* @returns {Promise<void>}
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
async _runModuleHook(hookName) {
|
|
139
|
+
for (const { name, instance } of this._modules.values()) {
|
|
140
|
+
const hook = instance?.[hookName];
|
|
141
|
+
|
|
142
|
+
if (typeof hook !== "function") {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await hook.call(instance, { config: this.config, logger: this.logger });
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw new ModuleError(
|
|
150
|
+
`Module "${name}" failed during "${hookName}": ${err.message}`,
|
|
151
|
+
{ moduleName: name, cause: err }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ------------------------------------------------------------------ */
|
|
158
|
+
/* Lifecycle: initialize / start / stop / status */
|
|
159
|
+
/* ------------------------------------------------------------------ */
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Initializes the ZAYRA system: loads configuration and runs each
|
|
163
|
+
* registered module's initialize() hook. Must be called before start().
|
|
164
|
+
*
|
|
165
|
+
* @returns {Promise<this>}
|
|
166
|
+
* @throws {ZayraError|ModuleError} If initialization fails.
|
|
167
|
+
*/
|
|
168
|
+
async initialize() {
|
|
169
|
+
if (this.state === ZayraState.INITIALIZING || this.state === ZayraState.INITIALIZED) {
|
|
170
|
+
this.logger.warn("initialize() called but Zayra is already initialized/initializing — ignoring.");
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.state = ZayraState.INITIALIZING;
|
|
175
|
+
this.logger.info(`Initializing ZAYRA core v${this.version}...`);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
if (this.autoLoadConfig) {
|
|
179
|
+
this.config = await loadConfig({ cwd: this.cwd });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await this._runModuleHook("initialize");
|
|
183
|
+
|
|
184
|
+
this.state = ZayraState.INITIALIZED;
|
|
185
|
+
this.logger.info("ZAYRA core initialized successfully.");
|
|
186
|
+
} catch (err) {
|
|
187
|
+
this.state = ZayraState.ERRORED;
|
|
188
|
+
this.logger.error("ZAYRA core failed to initialize.", err);
|
|
189
|
+
throw err instanceof ZayraError
|
|
190
|
+
? err
|
|
191
|
+
: new ZayraError(`Initialization failed: ${err.message}`, { cause: err });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Starts the ZAYRA system. Automatically runs initialize() first if
|
|
199
|
+
* it hasn't been run yet. Runs each registered module's start() hook.
|
|
200
|
+
*
|
|
201
|
+
* @returns {Promise<this>}
|
|
202
|
+
* @throws {ZayraError|ModuleError} If starting fails.
|
|
203
|
+
*/
|
|
204
|
+
async start() {
|
|
205
|
+
if (this.state === ZayraState.CREATED) {
|
|
206
|
+
await this.initialize();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (this.state === ZayraState.RUNNING) {
|
|
210
|
+
this.logger.warn("start() called but Zayra is already running — ignoring.");
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (this.state !== ZayraState.INITIALIZED && this.state !== ZayraState.STOPPED) {
|
|
215
|
+
throw new ZayraError(
|
|
216
|
+
`Cannot start ZAYRA core from state "${this.state}". Expected "initialized" or "stopped".`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.state = ZayraState.STARTING;
|
|
221
|
+
this.logger.info("Starting ZAYRA core...");
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await this._runModuleHook("start");
|
|
225
|
+
|
|
226
|
+
this.state = ZayraState.RUNNING;
|
|
227
|
+
this.logger.info("ZAYRA core is now running.");
|
|
228
|
+
} catch (err) {
|
|
229
|
+
this.state = ZayraState.ERRORED;
|
|
230
|
+
this.logger.error("ZAYRA core failed to start.", err);
|
|
231
|
+
throw err instanceof ZayraError
|
|
232
|
+
? err
|
|
233
|
+
: new ZayraError(`Start failed: ${err.message}`, { cause: err });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Stops the ZAYRA system. Runs each registered module's stop() hook.
|
|
241
|
+
*
|
|
242
|
+
* @returns {Promise<this>}
|
|
243
|
+
* @throws {ZayraError|ModuleError} If stopping fails.
|
|
244
|
+
*/
|
|
245
|
+
async stop() {
|
|
246
|
+
if (this.state === ZayraState.STOPPED || this.state === ZayraState.CREATED) {
|
|
247
|
+
this.logger.warn(`stop() called but Zayra is not running (state: "${this.state}") — ignoring.`);
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.state = ZayraState.STOPPING;
|
|
252
|
+
this.logger.info("Stopping ZAYRA core...");
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await this._runModuleHook("stop");
|
|
256
|
+
|
|
257
|
+
this.state = ZayraState.STOPPED;
|
|
258
|
+
this.logger.info("ZAYRA core stopped.");
|
|
259
|
+
} catch (err) {
|
|
260
|
+
this.state = ZayraState.ERRORED;
|
|
261
|
+
this.logger.error("ZAYRA core failed to stop cleanly.", err);
|
|
262
|
+
throw err instanceof ZayraError
|
|
263
|
+
? err
|
|
264
|
+
: new ZayraError(`Stop failed: ${err.message}`, { cause: err });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Returns a snapshot of the current core status. Synchronous and
|
|
272
|
+
* side-effect free — safe to call at any time, in any state.
|
|
273
|
+
*
|
|
274
|
+
* @returns {{ state: string, version: string, modules: string[], uptimeMs: number|null }}
|
|
275
|
+
*/
|
|
276
|
+
status() {
|
|
277
|
+
return {
|
|
278
|
+
state: this.state,
|
|
279
|
+
version: this.version,
|
|
280
|
+
modules: this.listModules(),
|
|
281
|
+
uptimeMs: this.state === ZayraState.RUNNING ? process.uptime() * 1000 : null,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export default Zayra;
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* errors.js
|
|
3
|
+
*
|
|
4
|
+
* Custom error hierarchy for ZAYRA.
|
|
5
|
+
*
|
|
6
|
+
* Design goal: never fail silently. Every error thrown inside
|
|
7
|
+
* zayra-core should be one of these classes (or a subclass of one),
|
|
8
|
+
* so callers can reliably do `if (err instanceof ConfigError)` etc.
|
|
9
|
+
* instead of parsing error messages.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base error class for all ZAYRA-related errors.
|
|
14
|
+
* Every other ZAYRA error extends this one.
|
|
15
|
+
*/
|
|
16
|
+
export class ZayraError extends Error {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} message - Human readable error message.
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param {string} [options.code] - Short machine-readable error code (e.g. "CORE_NOT_STARTED").
|
|
21
|
+
* @param {unknown} [options.cause] - Original error/cause, if this error wraps another one.
|
|
22
|
+
*/
|
|
23
|
+
constructor(message, options = {}) {
|
|
24
|
+
super(message);
|
|
25
|
+
|
|
26
|
+
this.name = this.constructor.name;
|
|
27
|
+
this.code = options.code ?? "ZAYRA_ERROR";
|
|
28
|
+
|
|
29
|
+
// Preserve original cause (e.g. a caught error) for debugging,
|
|
30
|
+
// without losing native Error.cause support if present.
|
|
31
|
+
if (options.cause !== undefined) {
|
|
32
|
+
this.cause = options.cause;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Maintains proper stack trace (V8 / Node specific, no-op elsewhere)
|
|
36
|
+
if (typeof Error.captureStackTrace === "function") {
|
|
37
|
+
Error.captureStackTrace(this, this.constructor);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Thrown when configuration loading/validation fails.
|
|
44
|
+
* Examples: missing required env var, invalid config.json, bad type.
|
|
45
|
+
*/
|
|
46
|
+
export class ConfigError extends ZayraError {
|
|
47
|
+
constructor(message, options = {}) {
|
|
48
|
+
super(message, { code: "CONFIG_ERROR", ...options });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when a module fails to load, initialize, start, or stop.
|
|
54
|
+
* Examples: module not found, module threw during initialize(), duplicate module name.
|
|
55
|
+
*/
|
|
56
|
+
export class ModuleError extends ZayraError {
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} message
|
|
59
|
+
* @param {object} [options]
|
|
60
|
+
* @param {string} [options.moduleName] - Name of the module that caused the error.
|
|
61
|
+
*/
|
|
62
|
+
constructor(message, options = {}) {
|
|
63
|
+
super(message, { code: "MODULE_ERROR", ...options });
|
|
64
|
+
this.moduleName = options.moduleName ?? null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default {
|
|
69
|
+
ZayraError,
|
|
70
|
+
ConfigError,
|
|
71
|
+
ModuleError,
|
|
72
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.js
|
|
3
|
+
*
|
|
4
|
+
* Public entry point for the zayra-core package.
|
|
5
|
+
*
|
|
6
|
+
* Anything a consumer of zayra-core needs should be exported from
|
|
7
|
+
* here — they should never need to import from "zayra-core/src/...".
|
|
8
|
+
* This keeps the internal file layout free to change without
|
|
9
|
+
* breaking downstream zayra-* packages.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { Zayra, ZayraState } from "./core.js";
|
|
13
|
+
export { loadConfig, requireConfigValue, getConfigValue } from "./config.js";
|
|
14
|
+
export { Logger, logger } from "./logger.js";
|
|
15
|
+
export { ZayraError, ConfigError, ModuleError } from "./errors.js";
|
|
16
|
+
export { VERSION } from "./version.js";
|
|
17
|
+
export * as utils from "./utils.js";
|
|
18
|
+
|
|
19
|
+
export { Zayra as default } from "./core.js";
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger.js
|
|
3
|
+
*
|
|
4
|
+
* Minimal, dependency-free logger for ZAYRA core.
|
|
5
|
+
*
|
|
6
|
+
* Kept intentionally simple in 0.0.1 — no external logging library,
|
|
7
|
+
* no file transports. The goal here is just consistent, readable
|
|
8
|
+
* console output that's easy to grep during debugging. A more
|
|
9
|
+
* advanced logger (file output, log levels from config, structured
|
|
10
|
+
* JSON logs) can replace this later without changing the public API
|
|
11
|
+
* (info/warn/error), since other zayra-* packages will depend on
|
|
12
|
+
* that shape.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const LEVEL_STYLES = {
|
|
16
|
+
info: { label: "INFO", prefix: "[ZAYRA]" },
|
|
17
|
+
warn: { label: "WARN", prefix: "[ZAYRA]" },
|
|
18
|
+
error: { label: "ERROR", prefix: "[ZAYRA]" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function timestamp() {
|
|
22
|
+
return new Date().toISOString();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Formats a single log line consistently across all levels.
|
|
27
|
+
*
|
|
28
|
+
* @param {"info"|"warn"|"error"} level
|
|
29
|
+
* @param {string} message
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function formatLine(level, message) {
|
|
33
|
+
const { prefix, label } = LEVEL_STYLES[level];
|
|
34
|
+
return `${prefix} ${timestamp()} ${label} ${message}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Logger class.
|
|
39
|
+
*
|
|
40
|
+
* Instantiable so future packages (zayra-config, zayra-memory, etc.)
|
|
41
|
+
* can create scoped loggers, e.g. `new Logger({ scope: "memory" })`,
|
|
42
|
+
* without this needing to change.
|
|
43
|
+
*/
|
|
44
|
+
export class Logger {
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} [options]
|
|
47
|
+
* @param {string} [options.scope] - Optional scope/module name shown in log lines.
|
|
48
|
+
*/
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.scope = options.scope ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_withScope(message) {
|
|
54
|
+
return this.scope ? `(${this.scope}) ${message}` : message;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log an informational message.
|
|
59
|
+
* @param {string} message
|
|
60
|
+
* @param {...unknown} args - Additional values to log (objects, errors, etc.)
|
|
61
|
+
*/
|
|
62
|
+
info(message, ...args) {
|
|
63
|
+
console.log(formatLine("info", this._withScope(message)), ...args);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Log a warning message.
|
|
68
|
+
* @param {string} message
|
|
69
|
+
* @param {...unknown} args
|
|
70
|
+
*/
|
|
71
|
+
warn(message, ...args) {
|
|
72
|
+
console.warn(formatLine("warn", this._withScope(message)), ...args);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Log an error message.
|
|
77
|
+
* @param {string} message
|
|
78
|
+
* @param {...unknown} args
|
|
79
|
+
*/
|
|
80
|
+
error(message, ...args) {
|
|
81
|
+
console.error(formatLine("error", this._withScope(message)), ...args);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Default shared logger instance, used internally by zayra-core.
|
|
87
|
+
* Most consumers can just import { logger } instead of constructing
|
|
88
|
+
* their own Logger instance.
|
|
89
|
+
*/
|
|
90
|
+
export const logger = new Logger();
|
|
91
|
+
|
|
92
|
+
export default logger;
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils.js
|
|
3
|
+
*
|
|
4
|
+
* Reusable helper functions shared across zayra-core.
|
|
5
|
+
* Grouped into: file handling, validation, formatting, path utilities.
|
|
6
|
+
*
|
|
7
|
+
* Kept dependency-free on purpose (only Node built-ins) so this
|
|
8
|
+
* package stays lightweight as the foundation for every other
|
|
9
|
+
* zayra-* package.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promises as fs } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
/* -------------------------------------------------------------------------- */
|
|
17
|
+
/* File handling */
|
|
18
|
+
/* -------------------------------------------------------------------------- */
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks whether a file or directory exists at the given path.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} targetPath
|
|
24
|
+
* @returns {Promise<boolean>}
|
|
25
|
+
*/
|
|
26
|
+
export async function fileExists(targetPath) {
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(targetPath);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reads and parses a JSON file.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} filePath
|
|
39
|
+
* @returns {Promise<any>} Parsed JSON content.
|
|
40
|
+
* @throws {SyntaxError} If the file content is not valid JSON.
|
|
41
|
+
*/
|
|
42
|
+
export async function readJsonFile(filePath) {
|
|
43
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Writes a JavaScript value to a file as formatted JSON.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} filePath
|
|
51
|
+
* @param {any} data
|
|
52
|
+
* @param {object} [options]
|
|
53
|
+
* @param {number} [options.indent=2]
|
|
54
|
+
* @returns {Promise<void>}
|
|
55
|
+
*/
|
|
56
|
+
export async function writeJsonFile(filePath, data, options = {}) {
|
|
57
|
+
const indent = options.indent ?? 2;
|
|
58
|
+
const content = JSON.stringify(data, null, indent);
|
|
59
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* -------------------------------------------------------------------------- */
|
|
63
|
+
/* Validation */
|
|
64
|
+
/* -------------------------------------------------------------------------- */
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns true if the value is a non-null plain object
|
|
68
|
+
* (not an array, not null, not a class instance like Map/Set).
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} value
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function isPlainObject(value) {
|
|
74
|
+
return (
|
|
75
|
+
typeof value === "object" &&
|
|
76
|
+
value !== null &&
|
|
77
|
+
!Array.isArray(value) &&
|
|
78
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns true if the value is a non-empty string (after trimming).
|
|
84
|
+
*
|
|
85
|
+
* @param {unknown} value
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
export function isNonEmptyString(value) {
|
|
89
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Asserts that a condition is true, throwing a TypeError with the
|
|
94
|
+
* given message otherwise. Useful for lightweight argument guards
|
|
95
|
+
* without pulling in a validation library.
|
|
96
|
+
*
|
|
97
|
+
* @param {boolean} condition
|
|
98
|
+
* @param {string} message
|
|
99
|
+
* @throws {TypeError}
|
|
100
|
+
*/
|
|
101
|
+
export function assert(condition, message) {
|
|
102
|
+
if (!condition) {
|
|
103
|
+
throw new TypeError(message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
/* Formatting */
|
|
109
|
+
/* -------------------------------------------------------------------------- */
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Formats a Date (defaults to now) as an ISO-8601 string.
|
|
113
|
+
*
|
|
114
|
+
* @param {Date} [date]
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function formatTimestamp(date = new Date()) {
|
|
118
|
+
return date.toISOString();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Safely stringifies a value for logging/debugging, falling back to
|
|
123
|
+
* String(value) if JSON.stringify throws (e.g. circular references).
|
|
124
|
+
*
|
|
125
|
+
* @param {unknown} value
|
|
126
|
+
* @param {number} [indent=2]
|
|
127
|
+
* @returns {string}
|
|
128
|
+
*/
|
|
129
|
+
export function safeStringify(value, indent = 2) {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.stringify(value, null, indent);
|
|
132
|
+
} catch {
|
|
133
|
+
return String(value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* -------------------------------------------------------------------------- */
|
|
138
|
+
/* Path utilities */
|
|
139
|
+
/* -------------------------------------------------------------------------- */
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolves the absolute directory name for an ES module, given its
|
|
143
|
+
* `import.meta.url`. Equivalent of CommonJS `__dirname` for ESM.
|
|
144
|
+
*
|
|
145
|
+
* Usage: const dir = dirnameFromMeta(import.meta.url);
|
|
146
|
+
*
|
|
147
|
+
* @param {string} metaUrl - Pass `import.meta.url` from the calling module.
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function dirnameFromMeta(metaUrl) {
|
|
151
|
+
return path.dirname(fileURLToPath(metaUrl));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Joins and resolves path segments into an absolute path.
|
|
156
|
+
*
|
|
157
|
+
* @param {...string} segments
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
export function resolvePath(...segments) {
|
|
161
|
+
return path.resolve(...segments);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default {
|
|
165
|
+
fileExists,
|
|
166
|
+
readJsonFile,
|
|
167
|
+
writeJsonFile,
|
|
168
|
+
isPlainObject,
|
|
169
|
+
isNonEmptyString,
|
|
170
|
+
assert,
|
|
171
|
+
formatTimestamp,
|
|
172
|
+
safeStringify,
|
|
173
|
+
dirnameFromMeta,
|
|
174
|
+
resolvePath,
|
|
175
|
+
};
|
package/src/version.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version.js
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the current zayra-core version.
|
|
5
|
+
* Kept in sync with package.json manually for now (0.0.1 era).
|
|
6
|
+
* Other modules should import VERSION from here instead of
|
|
7
|
+
* hardcoding a version string anywhere else in the codebase.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const VERSION = "0.0.1";
|
|
11
|
+
|
|
12
|
+
export default VERSION;
|