@mudah-cli/core 0.1.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.
@@ -0,0 +1,80 @@
1
+ import { ConfigRepository } from '@mudah-cli/config';
2
+ import { Container, type Abstract, type Constructor } from '@mudah-cli/container';
3
+ import { EventBus } from './events.js';
4
+ import { type MudahManifest } from './manifest.js';
5
+ import { ServiceProvider } from './service-provider.js';
6
+ export type ProviderClass = new (app: Application) => ServiceProvider;
7
+ export interface LazyProviderOptions {
8
+ /** Boot this provider when one of these commands is invoked. */
9
+ commands?: string[];
10
+ /** Boot this provider when one of these bindings is resolved. */
11
+ bindings?: readonly Abstract[];
12
+ /** Boot this provider when the predicate returns true (checked on demand). */
13
+ bootWhen?: (app: Application) => boolean;
14
+ }
15
+ /** A command module: a module whose default export is a command class. */
16
+ export interface CommandModule {
17
+ default: CommandClass;
18
+ }
19
+ export type CommandClass = Constructor<CommandShape>;
20
+ export interface CommandShape {
21
+ signature?: string;
22
+ description?: string;
23
+ handle: (...args: unknown[]) => unknown;
24
+ }
25
+ /** True when the value is a class with a `handle` prototype method. */
26
+ export declare function isCommandExport(value: unknown): value is CommandClass;
27
+ /**
28
+ * The Mudah application kernel.
29
+ *
30
+ * An IoC container with a service-provider boot lifecycle:
31
+ * two-phase provider boot (register → boot), lazy providers keyed on
32
+ * commands/bindings, and auto-discovery of `src/providers` and `src/commands`.
33
+ */
34
+ export declare class Application extends Container {
35
+ readonly basePath: string;
36
+ readonly manifest: MudahManifest;
37
+ private readonly providers;
38
+ private readonly lazyProviders;
39
+ private readonly bootedLazy;
40
+ private booted;
41
+ constructor(basePath?: string, manifest?: MudahManifest);
42
+ config(): ConfigRepository;
43
+ events(): EventBus;
44
+ /** Register a provider that always boots. */
45
+ register(provider: ProviderClass): this;
46
+ /**
47
+ * Register a provider that boots lazily — only when a matching command is
48
+ * invoked, a matching binding is resolved, or `bootWhen` returns true.
49
+ */
50
+ registerLazy(provider: ProviderClass, options?: LazyProviderOptions): this;
51
+ /**
52
+ * Boot all registered providers in two phases, each in registration order:
53
+ * 1. `register()` — container bindings
54
+ * 2. `boot()` — everything else
55
+ *
56
+ * Set `MUDAH_BOOT_PROFILE=1` to print per-hook timings on stderr.
57
+ */
58
+ boot(): Promise<void>;
59
+ /** Boot lazy providers that declared an interest in `command`. */
60
+ bootLazyForCommand(command: string): Promise<void>;
61
+ /** Boot lazy providers that declared an interest in `abstract`. */
62
+ bootLazyForBinding(abstract: Abstract): Promise<void>;
63
+ /** Boot lazy providers whose `bootWhen` predicate currently returns true. */
64
+ evaluateLazy(): Promise<void>;
65
+ private bootLazy;
66
+ /**
67
+ * Discover and register every `*.provider.{ts,js}` in the given directory
68
+ * (sorted for determinism). Exports whose name ends in `Provider` win.
69
+ */
70
+ discoverProviders(dir?: string): Promise<this>;
71
+ /**
72
+ * Discover command modules in the given directory (sorted). A command
73
+ * module is any file whose default export is a class with `handle`.
74
+ */
75
+ discoverCommandModules(dir?: string): Promise<CommandModule[]>;
76
+ /** Load an app-root-relative (or absolute) file as an ES module. */
77
+ importModule(path: string): Promise<Record<string, unknown>>;
78
+ private listFiles;
79
+ private importFile;
80
+ }
@@ -0,0 +1,180 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { ConfigRepository } from '@mudah-cli/config';
5
+ import { Container, isClassLike } from '@mudah-cli/container';
6
+ import { EventBus } from './events.js';
7
+ import { loadManifest } from './manifest.js';
8
+ import { ServiceProvider } from './service-provider.js';
9
+ /** True when the value is a class with a `handle` prototype method. */
10
+ export function isCommandExport(value) {
11
+ if (typeof value !== 'function' || !isClassLike(value))
12
+ return false;
13
+ return typeof value.prototype?.handle === 'function';
14
+ }
15
+ /**
16
+ * The Mudah application kernel.
17
+ *
18
+ * An IoC container with a service-provider boot lifecycle:
19
+ * two-phase provider boot (register → boot), lazy providers keyed on
20
+ * commands/bindings, and auto-discovery of `src/providers` and `src/commands`.
21
+ */
22
+ export class Application extends Container {
23
+ basePath;
24
+ manifest;
25
+ providers = [];
26
+ lazyProviders = [];
27
+ bootedLazy = new Set();
28
+ booted = false;
29
+ constructor(basePath = process.cwd(), manifest) {
30
+ super();
31
+ this.basePath = basePath;
32
+ this.manifest = manifest ?? loadManifest(basePath);
33
+ this.singleton('app', () => this);
34
+ this.singleton('config', () => new ConfigRepository());
35
+ this.singleton('events', () => new EventBus());
36
+ }
37
+ config() {
38
+ return this.make('config');
39
+ }
40
+ events() {
41
+ return this.make('events');
42
+ }
43
+ /** Register a provider that always boots. */
44
+ register(provider) {
45
+ this.providers.push(provider);
46
+ return this;
47
+ }
48
+ /**
49
+ * Register a provider that boots lazily — only when a matching command is
50
+ * invoked, a matching binding is resolved, or `bootWhen` returns true.
51
+ */
52
+ registerLazy(provider, options = {}) {
53
+ this.lazyProviders.push({ provider, options });
54
+ return this;
55
+ }
56
+ /**
57
+ * Boot all registered providers in two phases, each in registration order:
58
+ * 1. `register()` — container bindings
59
+ * 2. `boot()` — everything else
60
+ *
61
+ * Set `MUDAH_BOOT_PROFILE=1` to print per-hook timings on stderr.
62
+ */
63
+ async boot() {
64
+ if (this.booted)
65
+ return;
66
+ const profile = process.env['MUDAH_BOOT_PROFILE'] === '1';
67
+ const timings = [];
68
+ const started = performance.now();
69
+ for (const Provider of this.providers) {
70
+ const provider = new Provider(this);
71
+ const t0 = profile ? performance.now() : 0;
72
+ await provider.register?.();
73
+ if (profile)
74
+ timings.push(`${Provider.name}.register=${Math.round(performance.now() - t0)}ms`);
75
+ }
76
+ for (const Provider of this.providers) {
77
+ const provider = new Provider(this);
78
+ const t0 = profile ? performance.now() : 0;
79
+ await provider.boot?.();
80
+ if (profile)
81
+ timings.push(`${Provider.name}.boot=${Math.round(performance.now() - t0)}ms`);
82
+ }
83
+ if (profile) {
84
+ console.error(`[boot-profile] total=${Math.round(performance.now() - started)}ms ${timings.join(' ')}`);
85
+ }
86
+ this.booted = true;
87
+ await this.events().emit('app.booted', { app: this });
88
+ }
89
+ /** Boot lazy providers that declared an interest in `command`. */
90
+ async bootLazyForCommand(command) {
91
+ for (const entry of this.lazyProviders) {
92
+ if (this.bootedLazy.has(entry.provider))
93
+ continue;
94
+ if (entry.options.commands?.includes(command)) {
95
+ await this.bootLazy(entry.provider);
96
+ }
97
+ }
98
+ }
99
+ /** Boot lazy providers that declared an interest in `abstract`. */
100
+ async bootLazyForBinding(abstract) {
101
+ for (const entry of this.lazyProviders) {
102
+ if (this.bootedLazy.has(entry.provider))
103
+ continue;
104
+ if (entry.options.bindings?.some((binding) => binding === abstract)) {
105
+ await this.bootLazy(entry.provider);
106
+ }
107
+ }
108
+ }
109
+ /** Boot lazy providers whose `bootWhen` predicate currently returns true. */
110
+ async evaluateLazy() {
111
+ for (const entry of this.lazyProviders) {
112
+ if (this.bootedLazy.has(entry.provider))
113
+ continue;
114
+ if (entry.options.bootWhen?.(this)) {
115
+ await this.bootLazy(entry.provider);
116
+ }
117
+ }
118
+ }
119
+ async bootLazy(provider) {
120
+ if (this.bootedLazy.has(provider))
121
+ return;
122
+ this.bootedLazy.add(provider);
123
+ const instance = new provider(this);
124
+ await instance.register?.();
125
+ await instance.boot?.();
126
+ }
127
+ /**
128
+ * Discover and register every `*.provider.{ts,js}` in the given directory
129
+ * (sorted for determinism). Exports whose name ends in `Provider` win.
130
+ */
131
+ async discoverProviders(dir = join(this.basePath, 'src', 'providers')) {
132
+ for (const file of await this.listFiles(dir)) {
133
+ const mod = await this.importFile(file);
134
+ for (const value of Object.values(mod)) {
135
+ if (typeof value === 'function' && /Provider$/.test(value.name ?? '')) {
136
+ this.register(value);
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ return this;
142
+ }
143
+ /**
144
+ * Discover command modules in the given directory (sorted). A command
145
+ * module is any file whose default export is a class with `handle`.
146
+ */
147
+ async discoverCommandModules(dir = join(this.basePath, 'src', 'commands')) {
148
+ const modules = [];
149
+ for (const file of await this.listFiles(dir)) {
150
+ const mod = await this.importFile(file);
151
+ if (isCommandExport(mod.default)) {
152
+ modules.push(mod);
153
+ }
154
+ }
155
+ return modules;
156
+ }
157
+ /** Load an app-root-relative (or absolute) file as an ES module. */
158
+ async importModule(path) {
159
+ const resolved = path.startsWith('/') ? path : join(this.basePath, path);
160
+ return this.importFile(resolved);
161
+ }
162
+ async listFiles(dir) {
163
+ let entries;
164
+ try {
165
+ entries = await readdir(dir, { withFileTypes: true });
166
+ }
167
+ catch {
168
+ return [];
169
+ }
170
+ return entries
171
+ .filter((entry) => entry.isFile() && /\.(ts|mts|js|mjs)$/.test(entry.name))
172
+ .map((entry) => join(dir, entry.name))
173
+ .sort();
174
+ }
175
+ async importFile(file) {
176
+ const mod = await import(pathToFileURL(file).href);
177
+ return mod;
178
+ }
179
+ }
180
+ //# sourceMappingURL=application.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application.js","sourceRoot":"","sources":["../src/application.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { Application } from './application.js';
2
+ /** Typed application lifecycle events. */
3
+ export interface AppEvents {
4
+ 'app.booted': {
5
+ app: Application;
6
+ };
7
+ 'command.before': {
8
+ command: string;
9
+ argv: string[];
10
+ };
11
+ 'command.after': {
12
+ command: string;
13
+ exitCode: number;
14
+ durationMs: number;
15
+ };
16
+ 'command.error': {
17
+ command: string;
18
+ error: unknown;
19
+ };
20
+ }
21
+ export type EventHandler<E> = (payload: E) => void | Promise<void>;
22
+ /**
23
+ * Minimal typed event bus. Listeners run sequentially in registration order
24
+ * and are awaited, so a listener may do async work (notifications, telemetry).
25
+ */
26
+ export declare class EventBus {
27
+ private readonly listeners;
28
+ /** Subscribe. Returns an unsubscribe function. */
29
+ on<K extends keyof AppEvents>(event: K, handler: EventHandler<AppEvents[K]>): () => void;
30
+ off<K extends keyof AppEvents>(event: K, handler: EventHandler<AppEvents[K]>): void;
31
+ /** Emit to all subscribers, awaiting each in order. */
32
+ emit<K extends keyof AppEvents>(event: K, payload: AppEvents[K]): Promise<void>;
33
+ }
package/dist/events.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal typed event bus. Listeners run sequentially in registration order
3
+ * and are awaited, so a listener may do async work (notifications, telemetry).
4
+ */
5
+ export class EventBus {
6
+ listeners = new Map();
7
+ /** Subscribe. Returns an unsubscribe function. */
8
+ on(event, handler) {
9
+ let set = this.listeners.get(event);
10
+ if (!set) {
11
+ set = new Set();
12
+ this.listeners.set(event, set);
13
+ }
14
+ set.add(handler);
15
+ return () => this.off(event, handler);
16
+ }
17
+ off(event, handler) {
18
+ this.listeners.get(event)?.delete(handler);
19
+ }
20
+ /** Emit to all subscribers, awaiting each in order. */
21
+ async emit(event, payload) {
22
+ const set = this.listeners.get(event);
23
+ if (!set)
24
+ return;
25
+ for (const handler of [...set]) {
26
+ await handler(payload);
27
+ }
28
+ }
29
+ }
30
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAYA;;;GAGG;AACH,MAAM,OAAO,QAAQ;IACF,SAAS,GAAG,IAAI,GAAG,EAA6C,CAAC;IAElF,kDAAkD;IAClD,EAAE,CAA4B,KAAQ,EAAE,OAAmC;QACzE,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,OAA8B,CAAC,CAAC;QACxC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,GAAG,CAA4B,KAAQ,EAAE,OAAmC;QAC1E,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAA8B,CAAC,CAAC;IACpE,CAAC;IAED,uDAAuD;IACvD,KAAK,CAAC,IAAI,CAA4B,KAAQ,EAAE,OAAqB;QACnE,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;YAC/B,MAAO,OAAsC,CAAC,OAAO,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A usage error: bad arguments, missing flags, unknown command.
3
+ * Rendered with usage + hint, exits with code 2.
4
+ */
5
+ export declare class UsageError extends Error {
6
+ readonly hint?: string;
7
+ readonly usage?: string;
8
+ constructor(message: string, options?: {
9
+ hint?: string;
10
+ usage?: string;
11
+ });
12
+ }
13
+ /** Explicit exit with a chosen code (optionally with a message). */
14
+ export declare class ExitError extends Error {
15
+ readonly code: number;
16
+ constructor(code: number, message?: string);
17
+ }
18
+ /** The user interrupted a prompt (escape, ctrl+c). Exits with 130. */
19
+ export declare class CommandCancelled extends Error {
20
+ constructor();
21
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A usage error: bad arguments, missing flags, unknown command.
3
+ * Rendered with usage + hint, exits with code 2.
4
+ */
5
+ export class UsageError extends Error {
6
+ hint;
7
+ usage;
8
+ constructor(message, options = {}) {
9
+ super(message);
10
+ this.name = 'UsageError';
11
+ this.hint = options.hint;
12
+ this.usage = options.usage;
13
+ }
14
+ }
15
+ /** Explicit exit with a chosen code (optionally with a message). */
16
+ export class ExitError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.code = code;
21
+ this.name = 'ExitError';
22
+ }
23
+ }
24
+ /** The user interrupted a prompt (escape, ctrl+c). Exits with 130. */
25
+ export class CommandCancelled extends Error {
26
+ constructor() {
27
+ super('Command cancelled');
28
+ this.name = 'CommandCancelled';
29
+ }
30
+ }
31
+ //# sourceMappingURL=exceptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exceptions.js","sourceRoot":"","sources":["../src/exceptions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IAC1B,IAAI,CAAU;IACd,KAAK,CAAU;IAExB,YAAY,OAAe,EAAE,OAAO,GAAsC,EAAE;QAC1E,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAC7B,CAAC;CACF;AAED,oEAAoE;AACpE,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEvB,IAAI;IADf,YACW,IAAY,EACrB,OAAgB;QAEhB,KAAK,CAAC,OAAO,CAAC,CAAC;oBAHN,IAAI;QAIb,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF;AAED,sEAAsE;AACtE,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ export { Application, isCommandExport, type CommandClass, type CommandModule, type CommandShape, type LazyProviderOptions, type ProviderClass, } from './application.js';
2
+ export { EventBus, type AppEvents, type EventHandler } from './events.js';
3
+ export { CommandCancelled, ExitError, UsageError } from './exceptions.js';
4
+ export { loadManifest, MudahManifestError, type MudahManifest, type MudahUiOptions } from './manifest.js';
5
+ export { ServiceProvider } from './service-provider.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { Application, isCommandExport, } from './application.js';
2
+ export { EventBus } from './events.js';
3
+ export { CommandCancelled, ExitError, UsageError } from './exceptions.js';
4
+ export { loadManifest, MudahManifestError } from './manifest.js';
5
+ export { ServiceProvider } from './service-provider.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,eAAe,GAMhB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAqC,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAA2C,MAAM,eAAe,CAAC;AAC1G,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,29 @@
1
+ export interface MudahUiOptions {
2
+ /** Theme name (default `sleek`) or `auto` for dark/light detection. */
3
+ theme?: string;
4
+ /** Force reduced-motion rendering regardless of terminal detection. */
5
+ reducedMotion?: boolean;
6
+ /** Force color on (or off) regardless of TTY detection. */
7
+ color?: boolean;
8
+ }
9
+ export interface MudahManifest {
10
+ /** Application name (shown in help headers and notifications). */
11
+ name: string;
12
+ /** Application version (semver). */
13
+ version: string;
14
+ /** Binary name (`bin/<name>` in the published package). */
15
+ bin: string;
16
+ description?: string;
17
+ ui?: MudahUiOptions;
18
+ /** Set false to disable the update nudge. Default true. */
19
+ updates?: boolean;
20
+ /** Extra command file paths (relative to the app root) beyond src/commands. */
21
+ commands?: string[];
22
+ /** Extra provider file paths (relative to the app root) beyond src/providers. */
23
+ providers?: string[];
24
+ }
25
+ export declare class MudahManifestError extends Error {
26
+ constructor(message: string);
27
+ }
28
+ /** Read and validate `mudah.json` from the application root. */
29
+ export declare function loadManifest(basePath: string): MudahManifest;
@@ -0,0 +1,49 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export class MudahManifestError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'MudahManifestError';
7
+ }
8
+ }
9
+ /** Read and validate `mudah.json` from the application root. */
10
+ export function loadManifest(basePath) {
11
+ const file = join(basePath, 'mudah.json');
12
+ let raw;
13
+ try {
14
+ raw = JSON.parse(readFileSync(file, 'utf8'));
15
+ }
16
+ catch (error) {
17
+ const reason = error instanceof Error ? error.message : String(error);
18
+ throw new MudahManifestError(`No valid manifest at ${file} (${reason}). Scaffold an app with \`npm create @mudah-cli/mudah\` or add a mudah.json with "name", "version", and "bin".`);
19
+ }
20
+ if (typeof raw !== 'object' || raw === null) {
21
+ throw new MudahManifestError('mudah.json must contain a JSON object.');
22
+ }
23
+ const data = raw;
24
+ const rawName = data.name;
25
+ const rawVersion = data.version;
26
+ const rawBin = data.bin;
27
+ if (typeof rawName !== 'string' || rawName.length === 0) {
28
+ throw new MudahManifestError('mudah.json field "name" must be a non-empty string.');
29
+ }
30
+ if (typeof rawVersion !== 'string' || rawVersion.length === 0) {
31
+ throw new MudahManifestError('mudah.json field "version" must be a non-empty string.');
32
+ }
33
+ if (typeof rawBin !== 'string' || rawBin.length === 0) {
34
+ throw new MudahManifestError('mudah.json field "bin" must be a non-empty string.');
35
+ }
36
+ const manifest = { name: rawName, version: rawVersion, bin: rawBin };
37
+ if (typeof data.description === 'string')
38
+ manifest.description = data.description;
39
+ if (typeof data.updates === 'boolean')
40
+ manifest.updates = data.updates;
41
+ if (data.ui && typeof data.ui === 'object')
42
+ manifest.ui = data.ui;
43
+ if (Array.isArray(data.commands))
44
+ manifest.commands = data.commands.filter((c) => typeof c === 'string');
45
+ if (Array.isArray(data.providers))
46
+ manifest.providers = data.providers.filter((p) => typeof p === 'string');
47
+ return manifest;
48
+ }
49
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { Application } from './application.js';
2
+ /**
3
+ * Base class for Mudah service providers.
4
+ *
5
+ * - Override {@link register} to bind services into the container. Runs for
6
+ * every provider before any `boot()`.
7
+ * - Override {@link boot} for work that depends on other providers having
8
+ * registered (subscribing events, registering commands, …).
9
+ *
10
+ * Both hooks are async-first: use `async register()` when doing I/O — the
11
+ * kernel always awaits each hook in registration order.
12
+ */
13
+ export declare abstract class ServiceProvider {
14
+ protected readonly app: Application;
15
+ constructor(app: Application);
16
+ register(): void | Promise<void>;
17
+ boot(): void | Promise<void>;
18
+ /**
19
+ * Import a config file and merge it as defaults under `key`
20
+ * (existing values win).
21
+ */
22
+ protected mergeConfigFrom(path: string, key: string): Promise<void>;
23
+ }
@@ -0,0 +1,32 @@
1
+ import { isAbsolute, join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ /**
4
+ * Base class for Mudah service providers.
5
+ *
6
+ * - Override {@link register} to bind services into the container. Runs for
7
+ * every provider before any `boot()`.
8
+ * - Override {@link boot} for work that depends on other providers having
9
+ * registered (subscribing events, registering commands, …).
10
+ *
11
+ * Both hooks are async-first: use `async register()` when doing I/O — the
12
+ * kernel always awaits each hook in registration order.
13
+ */
14
+ export class ServiceProvider {
15
+ app;
16
+ constructor(app) {
17
+ this.app = app;
18
+ }
19
+ register() { }
20
+ boot() { }
21
+ /**
22
+ * Import a config file and merge it as defaults under `key`
23
+ * (existing values win).
24
+ */
25
+ async mergeConfigFrom(path, key) {
26
+ const resolved = isAbsolute(path) ? path : join(this.app.basePath, path);
27
+ const mod = await import(pathToFileURL(resolved).href);
28
+ const defaults = mod.default ?? mod;
29
+ this.app.config().merge(key, defaults);
30
+ }
31
+ }
32
+ //# sourceMappingURL=service-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-provider.js","sourceRoot":"","sources":["../src/service-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC;;;;;;;;;;GAUG;AACH,MAAM,OAAgB,eAAe;IACJ,GAAG;IAAlC,YAA+B,GAAgB;mBAAhB,GAAG;IAAgB,CAAC;IAEnD,QAAQ,KAA0B,CAAC;IAEnC,IAAI,KAA0B,CAAC;IAE/B;;;OAGG;IACO,KAAK,CAAC,eAAe,CAAC,IAAY,EAAE,GAAW;QACvD,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACzE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,QAAmC,CAAC,CAAC;IACpE,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@mudah-cli/core",
3
+ "version": "0.1.0",
4
+ "description": "Mudah application kernel: service providers, two-phase boot, events, and discovery.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=26"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "dependencies": {
21
+ "@mudah-cli/config": "^0.1.0",
22
+ "@mudah-cli/container": "^0.1.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ }
27
+ }