@crustjs/core 0.0.18 → 0.2.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/LICENSE +21 -0
- package/README.md +3 -48
- package/dist/index.d.ts +1203 -1209
- package/dist/index.js +232 -2
- package/dist/invocation-DcA5FqF7.js +1875 -0
- package/dist/tooling.d.ts +160 -0
- package/dist/tooling.js +96 -0
- package/dist/types-DjMHz7M6.d.ts +828 -0
- package/package.json +27 -11
- package/dist/shared/chunk-1670njz2.js +0 -2
- package/dist/shared/chunk-q8y07jw2.js +0 -3
package/dist/index.js
CHANGED
|
@@ -1,2 +1,232 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { _ as definingOf, a as runInvocation, b as normalizeFlag, d as installExtensionContexts, f as validateCommandSections, g as defineContext, h as defineExtension, i as prepareInvocation, m as registerFlag, o as snapshotCommand, p as createCommandNode, r as executeInvocation, u as cloneCommandNode, v as validateContextAvailability, x as CrustError, y as normalizeArg } from "./invocation-DcA5FqF7.js";
|
|
2
|
+
//#region src/identity.ts
|
|
3
|
+
/** Mint an Extension identity from any non-blank, trimmed string. */
|
|
4
|
+
function defineExtensionId(id) {
|
|
5
|
+
if (!id.trim() || id !== id.trim()) throw new CrustError("DEFINITION", "Extension id must be a non-empty, trimmed string", {
|
|
6
|
+
subject: "extension",
|
|
7
|
+
reason: "empty-id"
|
|
8
|
+
});
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/api/flags.ts
|
|
13
|
+
function defineFlag(name, def) {
|
|
14
|
+
return normalizeFlag(name, {
|
|
15
|
+
...def,
|
|
16
|
+
name
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function defineArg(name, def) {
|
|
20
|
+
return normalizeArg({
|
|
21
|
+
...def,
|
|
22
|
+
name
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/command/crust.ts
|
|
27
|
+
const commandDefinitionInternal = Symbol.for("crust.commandDefinition");
|
|
28
|
+
function materializeCommandDefinition(definition, parent, extensionName) {
|
|
29
|
+
const internal = definition[commandDefinitionInternal];
|
|
30
|
+
const name = definition.name;
|
|
31
|
+
const owner = extensionName ? `Extension "${extensionName}" command "${name}"` : `Command "${name}"`;
|
|
32
|
+
const definitionDetails = (reason) => ({
|
|
33
|
+
subject: extensionName ? "extension" : "command",
|
|
34
|
+
name: extensionName ?? name,
|
|
35
|
+
reason
|
|
36
|
+
});
|
|
37
|
+
const child = new Crust(name);
|
|
38
|
+
for (const [flagName, def] of Object.entries(parent.ownedFlags)) registerFlag(child._node, flagName, def, "owned");
|
|
39
|
+
child._node.contexts = parent.contexts.map((context) => ({ ...context }));
|
|
40
|
+
const configured = internal.recipe(child);
|
|
41
|
+
if (!(configured instanceof Crust) || configured._ancestorOwnedFlags !== child._ancestorOwnedFlags) throw new CrustError("DEFINITION", `${owner} definition must return the same command builder it received`, definitionDetails("foreign-command-builder"));
|
|
42
|
+
if (configured._node.extensions.length > 0) throw new CrustError("DEFINITION", `${owner} cannot register Extensions inside command definitions`, definitionDetails("nested-command-extension"));
|
|
43
|
+
const childNode = cloneCommandNode(configured._node);
|
|
44
|
+
validateContextAvailability(childNode.contexts.map(({ instance }) => instance), childNode.demands);
|
|
45
|
+
childNode.meta = {
|
|
46
|
+
name,
|
|
47
|
+
...internal.meta
|
|
48
|
+
};
|
|
49
|
+
return childNode;
|
|
50
|
+
}
|
|
51
|
+
function resolveCommandName(name, aliases = []) {
|
|
52
|
+
if (name.trim() === "") throw new CrustError("DEFINITION", "Command name must be a non-empty string", {
|
|
53
|
+
subject: "command",
|
|
54
|
+
name,
|
|
55
|
+
reason: "empty-name"
|
|
56
|
+
});
|
|
57
|
+
if (name === "__proto__") throw new CrustError("DEFINITION", "Command name \"__proto__\" is reserved", {
|
|
58
|
+
subject: "command",
|
|
59
|
+
name,
|
|
60
|
+
reason: "reserved-name"
|
|
61
|
+
});
|
|
62
|
+
if (aliases.includes(name)) throw new CrustError("DEFINITION", `Command "${name}" must not list its own canonical name as an alias`, {
|
|
63
|
+
subject: "command",
|
|
64
|
+
name,
|
|
65
|
+
reason: "alias-collision"
|
|
66
|
+
});
|
|
67
|
+
return name;
|
|
68
|
+
}
|
|
69
|
+
function isCommandRecipe(value) {
|
|
70
|
+
return typeof value === "function";
|
|
71
|
+
}
|
|
72
|
+
function defineCommand(nameInput, configOrRecipe, maybeRecipe) {
|
|
73
|
+
const hasConfig = !isCommandRecipe(configOrRecipe);
|
|
74
|
+
const config = hasConfig ? configOrRecipe : {};
|
|
75
|
+
const recipe = hasConfig ? maybeRecipe : configOrRecipe;
|
|
76
|
+
const name = resolveCommandName(nameInput, config.aliases);
|
|
77
|
+
for (const alias of config.aliases ?? []) if (alias === "" || /[ \t\n\r\v\f]/.test(alias) || alias.startsWith("-")) throw new CrustError("DEFINITION", `Command "${name}" has an invalid alias "${alias}"`);
|
|
78
|
+
const { sections, version: _rootVersion, ...metaRest } = config;
|
|
79
|
+
const meta = {
|
|
80
|
+
...metaRest,
|
|
81
|
+
...config.aliases ? { aliases: [...config.aliases] } : {},
|
|
82
|
+
...sections ? { sections: validateCommandSections(name, sections) } : {}
|
|
83
|
+
};
|
|
84
|
+
const internal = {
|
|
85
|
+
name,
|
|
86
|
+
recipe,
|
|
87
|
+
meta
|
|
88
|
+
};
|
|
89
|
+
const named = (defName) => {
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
name: defName,
|
|
92
|
+
as: (newName) => named(resolveCommandName(newName, meta.aliases)),
|
|
93
|
+
[commandDefinitionInternal]: Object.freeze({
|
|
94
|
+
...internal,
|
|
95
|
+
name: defName
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
return named(name);
|
|
100
|
+
}
|
|
101
|
+
function dedupeExtensions(extensions) {
|
|
102
|
+
return extensions.filter((e, i) => extensions.findLastIndex((x) => x.id === e.id) === i);
|
|
103
|
+
}
|
|
104
|
+
var Crust = class {
|
|
105
|
+
/** @internal */
|
|
106
|
+
_node;
|
|
107
|
+
/** @internal — Recipe-builder lineage anchor, unique per materialization and preserved by clones */
|
|
108
|
+
_ancestorOwnedFlags;
|
|
109
|
+
constructor(nameInput, ...metadata) {
|
|
110
|
+
const meta = metadata[0] ?? {};
|
|
111
|
+
const name = resolveCommandName(nameInput);
|
|
112
|
+
this._node = createCommandNode(name);
|
|
113
|
+
if (meta.description !== void 0) this._node.meta.description = meta.description;
|
|
114
|
+
if (meta.version !== void 0) this._node.meta.version = meta.version;
|
|
115
|
+
if (meta.usage !== void 0) this._node.meta.usage = meta.usage;
|
|
116
|
+
if (meta.sections !== void 0) this._node.meta.sections = validateCommandSections(name, meta.sections);
|
|
117
|
+
this._ancestorOwnedFlags = {};
|
|
118
|
+
}
|
|
119
|
+
/** @internal — Clone this builder with a new node, preserving generics. */
|
|
120
|
+
_clone(nodeOverrides) {
|
|
121
|
+
const cloned = Object.create(Object.getPrototypeOf(this));
|
|
122
|
+
cloned._node = {
|
|
123
|
+
...cloneCommandNode({
|
|
124
|
+
...this._node,
|
|
125
|
+
subCommands: {}
|
|
126
|
+
}),
|
|
127
|
+
subCommands: { ...this._node.subCommands },
|
|
128
|
+
...nodeOverrides
|
|
129
|
+
};
|
|
130
|
+
cloned._ancestorOwnedFlags = this._ancestorOwnedFlags;
|
|
131
|
+
return cloned;
|
|
132
|
+
}
|
|
133
|
+
flags(...defs) {
|
|
134
|
+
const cloned = this._clone({});
|
|
135
|
+
for (const def of defs) {
|
|
136
|
+
const { name, ...rest } = def;
|
|
137
|
+
registerFlag(cloned._node, name, rest, "local");
|
|
138
|
+
}
|
|
139
|
+
return cloned;
|
|
140
|
+
}
|
|
141
|
+
args(...defs) {
|
|
142
|
+
const combined = [...this._node.args, ...defs.map(normalizeArg)];
|
|
143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
144
|
+
for (const [index, definition] of combined.entries()) {
|
|
145
|
+
if (seen.has(definition.name)) throw new CrustError("DEFINITION", `Argument name "${definition.name}" is already defined`, {
|
|
146
|
+
subject: "argument",
|
|
147
|
+
name: definition.name,
|
|
148
|
+
reason: "duplicate-arg"
|
|
149
|
+
});
|
|
150
|
+
seen.add(definition.name);
|
|
151
|
+
if (definition.variadic === true && index !== combined.length - 1) throw new CrustError("DEFINITION", `Only the last positional argument can be variadic; "${definition.name}" is not last`, {
|
|
152
|
+
subject: "argument",
|
|
153
|
+
name: definition.name,
|
|
154
|
+
reason: "variadic-position"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return this._clone({ args: combined });
|
|
158
|
+
}
|
|
159
|
+
use(...factories) {
|
|
160
|
+
return this._clone({ demands: [...this._node.demands, ...factories.map(definingOf)] });
|
|
161
|
+
}
|
|
162
|
+
provide(...inputs) {
|
|
163
|
+
const instances = inputs.map(definingOf);
|
|
164
|
+
validateContextAvailability([...this._node.contexts.map(({ instance }) => instance), ...instances], instances);
|
|
165
|
+
const cloned = this._clone({ contexts: [...this._node.contexts, ...instances.map((instance) => ({ instance }))] });
|
|
166
|
+
for (const instance of instances) for (const [name, definition] of Object.entries(instance.ownedFlags)) registerFlag(cloned._node, name, definition, "owned");
|
|
167
|
+
return cloned;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Define the Command Action — the function that implements this
|
|
171
|
+
* command's behavior after its inputs are ready.
|
|
172
|
+
*
|
|
173
|
+
* The action receives a {@link CrustCommandContext} with `args` typed from
|
|
174
|
+
* `.args()` and `flags` typed from the accumulated `Flags`.
|
|
175
|
+
*
|
|
176
|
+
* Calling `.action()` again replaces the command behavior on the new builder.
|
|
177
|
+
* The original builder is not mutated.
|
|
178
|
+
*
|
|
179
|
+
* @param action - The Command Action function
|
|
180
|
+
* @returns A new `Crust` instance with the action registered
|
|
181
|
+
*/
|
|
182
|
+
action(action) {
|
|
183
|
+
return this._clone({ run: action });
|
|
184
|
+
}
|
|
185
|
+
extend(...inputs) {
|
|
186
|
+
const extensions = inputs.map(definingOf);
|
|
187
|
+
const activeExtensions = dedupeExtensions([...this._node.extensions, ...extensions]);
|
|
188
|
+
const node = installExtensionContexts(this._node, activeExtensions, new Set(extensions.map((extension) => extension.id)));
|
|
189
|
+
validateContextAvailability(node.contexts.map(({ instance }) => instance), activeExtensions.flatMap((extension) => [...extension.uses, ...extension.provides ?? []]));
|
|
190
|
+
return this._clone({
|
|
191
|
+
...node,
|
|
192
|
+
extensions: activeExtensions
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
add(...definitions) {
|
|
196
|
+
return this._addDefinitions(definitions);
|
|
197
|
+
}
|
|
198
|
+
command(name, recipe) {
|
|
199
|
+
const definition = defineCommand(name, recipe);
|
|
200
|
+
return this._addDefinitions([definition]);
|
|
201
|
+
}
|
|
202
|
+
_addDefinitions(definitions) {
|
|
203
|
+
const subCommands = { ...this._node.subCommands };
|
|
204
|
+
for (const definition of definitions) {
|
|
205
|
+
const spellings = [definition.name, ...definition[commandDefinitionInternal].meta.aliases ?? []];
|
|
206
|
+
for (const sibling of Object.values(subCommands)) if ([sibling.meta.name, ...sibling.meta.aliases ?? []].some((name) => spellings.includes(name))) throw new CrustError("DEFINITION", `Command name "${definition.name}" is already registered on this command`, {
|
|
207
|
+
subject: "command",
|
|
208
|
+
name: definition.name,
|
|
209
|
+
reason: "command-collision"
|
|
210
|
+
});
|
|
211
|
+
const childNode = materializeCommandDefinition(definition, this._node);
|
|
212
|
+
subCommands[definition.name] = childNode;
|
|
213
|
+
}
|
|
214
|
+
return this._clone({ subCommands });
|
|
215
|
+
}
|
|
216
|
+
async snapshot() {
|
|
217
|
+
return snapshotCommand(prepareInvocation(this._node, materializeCommandDefinition).rootNode);
|
|
218
|
+
}
|
|
219
|
+
async run(path, ...args) {
|
|
220
|
+
const structuredInput = args[0] ?? {};
|
|
221
|
+
const io = args[1];
|
|
222
|
+
return await runInvocation(this._node, {
|
|
223
|
+
path,
|
|
224
|
+
input: structuredInput
|
|
225
|
+
}, io, materializeCommandDefinition);
|
|
226
|
+
}
|
|
227
|
+
async execute(options) {
|
|
228
|
+
return await executeInvocation(this._node, options, materializeCommandDefinition);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
//#endregion
|
|
232
|
+
export { Crust, CrustError, defineArg, defineCommand, defineContext, defineExtension, defineExtensionId, defineFlag };
|