EdgeAIG's picture
download
raw
26.2 kB
import * as Console from "../../Console.js";
import * as Context from "../../Context.js";
import * as Effect from "../../Effect.js";
import { dual } from "../../Function.js";
import * as Option from "../../Option.js";
import * as Predicate from "../../Predicate.js";
import * as References from "../../References.js";
import * as Result from "../../Result.js";
import * as Stdio from "../../Stdio.js";
import * as Terminal from "../../Terminal.js";
import * as CliError from "./CliError.js";
import * as CliOutput from "./CliOutput.js";
import * as GlobalFlag from "./GlobalFlag.js";
import { checkForDuplicateFlags, makeCommand, makeParser, toImpl, TypeId } from "./internal/command.js";
import { mergeConfig, parseConfig } from "./internal/config.js";
import { getGlobalFlagsForCommandPath, getGlobalFlagsForCommandTree, getHelpForCommandPath } from "./internal/help.js";
import * as Lexer from "./internal/lexer.js";
import * as Parser from "./internal/parser.js";
import * as Param from "./Param.js";
/**
* Returns `true` if the provided value is a `Command`.
*
* **Gotchas**
*
* This checks for the `Command` type-id property; it does not validate the full
* command shape.
*
* @category guards
* @since 4.0.0
*/
export const isCommand = u => Predicate.hasProperty(u, TypeId);
/* ========================================================================== */
/* Constructors */
/* ========================================================================== */
/**
* Creates a `Command` from a name, an optional configuration, and an optional
* handler.
*
* **Details**
*
* Use `withDescription` and related metadata combinators to add help text. The
* overloads support simple commands, configured commands, and commands with
* effectful handlers.
*
* **Example** (Creating commands)
*
* ```ts
* import { Console, Effect } from "effect"
* import { Argument, Command, Flag } from "effect/unstable/cli"
*
* // Simple command with no configuration
* const version = Command.make("version")
*
* // Command with simple flags
* const greet = Command.make("greet", {
* name: Flag.string("name"),
* count: Flag.integer("count").pipe(Flag.withDefault(1))
* })
*
* // Command with nested configuration
* const deploy = Command.make("deploy", {
* environment: Flag.string("env").pipe(
* Flag.withDescription("Target environment")
* ),
* server: {
* host: Flag.string("host").pipe(Flag.withDefault("localhost")),
* port: Flag.integer("port").pipe(Flag.withDefault(3000))
* },
* files: Argument.string("files").pipe(Argument.variadic),
* force: Flag.boolean("force").pipe(Flag.withDescription("Force deployment"))
* })
*
* // Command with handler
* const deployWithHandler = Command.make("deploy", {
* environment: Flag.string("env"),
* force: Flag.boolean("force")
* }, (config) =>
* Effect.gen(function*() {
* yield* Console.log(`Starting deployment to ${config.environment}`)
*
* if (!config.force && config.environment === "production") {
* return yield* Effect.fail("Production deployments require --force flag")
* }
*
* yield* Console.log("Deployment completed successfully")
* }))
* ```
*
* @category constructors
* @since 4.0.0
*/
export const make = (name, config, handler) => {
const parsedConfig = parseConfig(config ?? {});
return makeCommand({
name,
config: parsedConfig,
...(Predicate.isNotUndefined(handler) ? {
handle: handler
} : {})
});
};
/* ========================================================================== */
/* Combinators */
/* ========================================================================== */
/**
* Adds or replaces the handler for a command.
*
* **Example** (Adding command handlers)
*
* ```ts
* import { Console } from "effect"
* import { Command, Flag } from "effect/unstable/cli"
*
* // Command without initial handler
* const greet = Command.make("greet", {
* name: Flag.string("name")
* })
*
* // Add handler later
* const greetWithHandler = greet.pipe(
* Command.withHandler((config: { readonly name: string }) =>
* Console.log(`Hello, ${config.name}!`)
* )
* )
* ```
*
* @category combinators
* @since 4.0.0
*/
export const withHandler = /*#__PURE__*/dual(2, (self, handler) => makeCommand({
...toImpl(self),
handle: handler
}));
const normalizeSubcommandEntries = entries => {
const flat = [];
const grouped = new Map();
const addToGroup = (group, command) => {
flat.push(command);
const existing = grouped.get(group);
if (existing) {
existing.push(command);
} else {
grouped.set(group, [command]);
}
};
for (const entry of entries) {
if (isCommand(entry)) {
addToGroup(undefined, entry);
continue;
}
for (const command of entry.commands) {
addToGroup(entry.group, command);
}
}
const groups = [];
const ungroupedCommands = grouped.get(undefined);
if (ungroupedCommands && ungroupedCommands.length > 0) {
groups.push({
group: undefined,
commands: ungroupedCommands
});
}
for (const [group, commands] of grouped) {
if (group === undefined) {
continue;
}
groups.push({
group,
commands
});
}
return {
flat,
groups
};
};
/**
* Adds subcommands to a command, creating a hierarchical command structure.
*
* **Details**
*
* Subcommands can access their parent's parsed configuration by yielding the parent
* command within their handler. This enables shared parent flags that affect
* all subcommands.
*
* **Example** (Adding subcommands)
*
* ```ts
* import { Console, Effect } from "effect"
* import { Command, Flag } from "effect/unstable/cli"
*
* // Parent command with shared flags
* const git = Command.make("git").pipe(
* Command.withSharedFlags({
* verbose: Flag.boolean("verbose")
* })
* )
*
* // Subcommand that accesses parent config
* const clone = Command.make("clone", {
* repository: Flag.string("repo")
* }, (config) =>
* Effect.gen(function*() {
* const parent = yield* git // Access parent's parsed config
* if (parent.verbose) {
* yield* Console.log("Verbose mode enabled")
* }
* yield* Console.log(`Cloning ${config.repository}`)
* }))
*
* const app = git.pipe(Command.withSubcommands([clone]))
* // Usage: git --verbose clone --repo github.com/foo/bar
* ```
*
* @category combinators
* @since 4.0.0
*/
export const withSubcommands = /*#__PURE__*/dual(2, (self, subcommands) => {
const normalized = normalizeSubcommandEntries(subcommands);
checkForDuplicateFlags(self, normalized.flat);
const impl = toImpl(self);
const byName = new Map(normalized.flat.map(s => [s.name, toImpl(s)]));
const SubcommandStateSymbol = Symbol("effect/cli/SubcommandState");
const parse = Effect.fnUntraced(function* (raw) {
if (Option.isNone(raw.subcommand)) {
return yield* impl.parse(raw);
}
const sub = byName.get(raw.subcommand.value.name);
if (!sub) {
return yield* impl.parse(raw);
}
const context = yield* impl.parseContext(raw);
const result = yield* sub.parse(raw.subcommand.value.parsedInput);
return Object.assign({}, context, {
[SubcommandStateSymbol]: {
name: sub.name,
result
}
});
});
const handle = Effect.fnUntraced(function* (input, path) {
const internal = input;
const selectedSubcommand = internal[SubcommandStateSymbol];
if (selectedSubcommand) {
const child = byName.get(selectedSubcommand.name);
if (!child) {
return yield* new CliError.ShowHelp({
commandPath: path,
errors: []
});
}
return yield* child.handle(selectedSubcommand.result, [...path, child.name]).pipe(Effect.provideService(impl.service, input));
}
return yield* impl.handle(input, path);
});
return makeCommand({
name: impl.name,
config: impl.config,
contextConfig: impl.contextConfig,
description: impl.description,
shortDescription: impl.shortDescription,
alias: impl.alias,
annotations: impl.annotations,
globalFlags: impl.globalFlags,
examples: impl.examples,
service: impl.service,
subcommands: normalized.groups,
parse,
parseContext: impl.parseContext,
handle
});
});
/**
* Adds flags that are inherited by subcommands.
*
* **Details**
*
* Shared flags are available to this command's handler and to descendant
* handlers via `yield* parentCommand`. Shared flags are accepted both before
* and after a selected subcommand name (npm-style).
*
* @category combinators
* @since 4.0.0
*/
export const withSharedFlags = /*#__PURE__*/dual(2, (self, sharedFlags) => {
const impl = toImpl(self);
const sharedConfig = parseConfig(sharedFlags);
const mergedConfig = mergeConfig(impl.config, sharedConfig);
const mergedContextConfig = mergeConfig(impl.contextConfig, sharedConfig);
if (impl.subcommands.length > 0) {
const flatSubcommands = impl.subcommands.flatMap(group => group.commands);
checkForDuplicateFlags(self, flatSubcommands, {
contextConfig: mergedContextConfig
});
}
const parseShared = makeParser(sharedConfig);
const parse = Effect.fnUntraced(function* (raw) {
const base = yield* impl.parse(raw);
const shared = yield* parseShared(raw);
return Object.assign({}, base, shared);
});
const parseContext = Effect.fnUntraced(function* (raw) {
const base = yield* impl.parseContext(raw);
const shared = yield* parseShared(raw);
return Object.assign({}, base, shared);
});
const handle = (input, commandPath) => impl.handle(input, commandPath);
return makeCommand({
name: impl.name,
config: mergedConfig,
contextConfig: mergedContextConfig,
description: impl.description,
shortDescription: impl.shortDescription,
alias: impl.alias,
annotations: impl.annotations,
globalFlags: impl.globalFlags,
examples: impl.examples,
service: impl.service,
subcommands: impl.subcommands,
parse,
parseContext,
handle
});
});
/**
* Adds global flags to a command scope.
*
* **Details**
*
* Declared global flags apply to the command and all of its descendants.
*
* @category combinators
* @since 4.0.0
*/
export const withGlobalFlags = /*#__PURE__*/dual(2, (self, globalFlags) => {
const impl = toImpl(self);
const next = Array.from(new Set([...impl.globalFlags, ...globalFlags]));
return makeCommand({
...impl,
globalFlags: next
});
});
/**
* Sets the description for a command.
*
* **Details**
*
* Descriptions provide users with information about what the command does
* when they view help documentation.
*
* **Example** (Setting descriptions)
*
* ```ts
* import { Console, Effect } from "effect"
* import { Command, Flag } from "effect/unstable/cli"
*
* const deploy = Command.make("deploy", {
* environment: Flag.string("env")
* }, (config) =>
* Effect.gen(function*() {
* yield* Console.log(`Deploying to ${config.environment}`)
* })).pipe(
* Command.withDescription("Deploy the application to a specified environment")
* )
* ```
*
* @category combinators
* @since 4.0.0
*/
export const withDescription = /*#__PURE__*/dual(2, (self, description) => makeCommand({
...toImpl(self),
description
}));
/**
* Sets a short description for a command.
*
* **Details**
*
* Short descriptions are used when listing subcommands in help output and
* shell completions. If no short description is provided, the full
* `description` is used as a fallback.
*
* @category combinators
* @since 4.0.0
*/
export const withShortDescription = /*#__PURE__*/dual(2, (self, shortDescription) => makeCommand({
...toImpl(self),
shortDescription
}));
/**
* Sets an alias for a command.
*
* **Details**
*
* Aliases are accepted as alternate subcommand names during parsing and are
* shown in help output as `name, alias`.
*
* @category combinators
* @since 4.0.0
*/
export const withAlias = /*#__PURE__*/dual(2, (self, alias) => makeCommand({
...toImpl(self),
alias
}));
/**
* Hides a subcommand from parent help output, shell completions, and
* "did you mean?" suggestions while keeping it fully invocable by exact name.
*
* **When to use**
*
* Use when experimental or internal subcommands should be accepted but not advertised on
* the public CLI surface.
*
* **Example** (Hiding a subcommand)
*
* ```ts
* import { Command } from "effect/unstable/cli"
*
* // `experimental` still runs when invoked as `mycli experimental`,
* // but it does not appear under SUBCOMMANDS in `mycli --help`.
* const experimental = Command.make("experimental").pipe(
* Command.withHidden
* )
*
* const root = Command.make("mycli").pipe(
* Command.withSubcommands([experimental])
* )
* ```
*
* @category combinators
* @since 4.0.0
*/
export const withHidden = self => makeCommand({
...toImpl(self),
hidden: true
});
/**
* Adds a custom annotation to a command.
*
* **When to use**
*
* Use to attach one command-scoped metadata value under a `Context.Key`,
* especially for consumers such as custom help formatters.
*
* **Details**
*
* Annotations are stored on the command's annotation context and flow into
* generated help document annotations.
*
* **Gotchas**
*
* Adding the same `Context.Key` again replaces the earlier value.
*
* @see {@link annotateMerge} for merging an existing annotation context
*
* @category combinators
* @since 4.0.0
*/
export const annotate = /*#__PURE__*/dual(3, (self, service, value) => {
const impl = toImpl(self);
return makeCommand({
...impl,
annotations: Context.add(impl.annotations, service, value)
});
});
/**
* Merges a Context of annotations into a command.
*
* **When to use**
*
* Use when you need to attach an already-built `Context.Context` of command
* annotations.
*
* **Details**
*
* Merged annotations are stored on the command and exposed through generated
* help document annotations.
*
* **Gotchas**
*
* If both contexts contain the same `Context.Key`, the incoming annotations
* context wins.
*
* @see {@link annotate} for adding a single annotation without constructing a `Context`
*
* @category combinators
* @since 4.0.0
*/
export const annotateMerge = /*#__PURE__*/dual(2, (self, annotations) => {
const impl = toImpl(self);
return makeCommand({
...impl,
annotations: Context.merge(impl.annotations, annotations)
});
});
/**
* Sets usage examples for a command.
*
* **Details**
*
* Examples are exposed in structured `HelpDoc` data and rendered by the
* default formatter in an `EXAMPLES` section.
*
* **Example** (Adding usage examples)
*
* ```ts
* import { Command } from "effect/unstable/cli"
*
* const login = Command.make("login").pipe(
* Command.withExamples([
* { command: "myapp login", description: "Log in with browser OAuth" },
* { command: "myapp login --token sbp_abc123", description: "Log in with a token" }
* ])
* )
* ```
*
* @category combinators
* @since 4.0.0
*/
export const withExamples = /*#__PURE__*/dual(2, (self, examples) => makeCommand({
...toImpl(self),
examples
}));
/* ========================================================================== */
/* Providing Services */
/* ========================================================================== */
// Internal helper: transforms a command's handler while preserving other properties
const mapHandler = (self, f) => {
const impl = toImpl(self);
return makeCommand({
...impl,
handle: (input, path) => f(impl.handle(input, path), input)
});
};
/**
* Provides the handler of a command with the services produced by a layer
* that optionally depends on the command-line input to be created.
*
* **Example** (Providing command services)
*
* ```ts
* import { Effect, FileSystem, PlatformError } from "effect"
* import { Command, Flag } from "effect/unstable/cli"
*
* const deploy = Command.make("deploy", {
* env: Flag.string("env")
* }, (config) =>
* Effect.gen(function*() {
* const fs = yield* FileSystem.FileSystem
* // Use fs...
* })).pipe(
* // Provide FileSystem based on the --env flag
* Command.provide((config) =>
* config.env === "local"
* ? FileSystem.layerNoop({})
* : FileSystem.layerNoop({
* access: () =>
* Effect.fail(
* PlatformError.badArgument({
* module: "FileSystem",
* method: "access"
* })
* )
* })
* )
* )
* ```
*
* @category providing services
* @since 4.0.0
*/
export const provide = /*#__PURE__*/dual(args => isCommand(args[0]), (self, layer, options) => mapHandler(self, (handler, input) => Effect.provide(handler, typeof layer === "function" ? layer(input) : layer, options)));
/**
* Provides the handler of a command with the implementation of a service that
* optionally depends on the command-line input to be constructed.
*
* **When to use**
*
* Use when a command handler needs a pure service implementation, optionally
* derived from the parsed command input.
*
* @category providing services
* @since 4.0.0
*/
export const provideSync = /*#__PURE__*/dual(3, (self, service, implementation) => mapHandler(self, (handler, input) => Effect.provideService(handler, service, typeof implementation === "function" ? implementation(input) : implementation)));
/**
* Provides the handler of a command with the service produced by an effect
* that optionally depends on the command-line input to be created.
*
* **When to use**
*
* Use to acquire a service effectfully for each command run, optionally using
* parsed command input.
*
* @see {@link provideSync} for synchronous service acquisition
* @see {@link provide} for providing an already-available service
* @see {@link provideEffectDiscard} for running an effect before the handler without providing a service
*
* @category providing services
* @since 4.0.0
*/
export const provideEffect = /*#__PURE__*/dual(3, (self, service, effect) => mapHandler(self, (handler, input) => Effect.provideServiceEffect(handler, service, typeof effect === "function" ? effect(input) : effect)));
/**
* Allows for execution of an effect, which optionally depends on command-line
* input to be created, prior to executing the handler of a command.
*
* @category providing services
* @since 4.0.0
*/
export const provideEffectDiscard = /*#__PURE__*/dual(2, (self, effect) => mapHandler(self, (handler, input) => Effect.andThen(typeof effect === "function" ? effect(input) : effect, handler)));
/* ========================================================================== */
/* Execution */
/* ========================================================================== */
const getOutOfScopeGlobalFlagErrors = (allFlags, activeFlags, flagMap, commandPath) => {
const activeSet = new Set(activeFlags);
const errors = [];
const seen = new Set();
for (const flag of allFlags) {
if (activeSet.has(flag)) {
continue;
}
const singles = Param.extractSingleParams(flag.flag);
for (const single of singles) {
const entries = flagMap[single.name];
if (!entries || entries.length === 0) {
continue;
}
const option = `--${single.name}`;
if (seen.has(option)) {
continue;
}
seen.add(option);
errors.push(new CliError.UnrecognizedOption({
option,
suggestions: [],
command: commandPath
}));
}
}
return errors;
};
const showHelp = (command, error) => Effect.gen(function* () {
const formatter = yield* CliOutput.Formatter;
const helpDoc = yield* getHelpForCommandPath(command, error.commandPath, GlobalFlag.BuiltIns);
yield* Console.log(formatter.formatHelpDoc(helpDoc));
if (error.errors.length > 0) {
yield* Console.error(formatter.formatErrors(error.errors));
}
});
/**
* Runs a command using the arguments supplied by the `Stdio` service.
*
* **When to use**
*
* Use when command-line arguments should come from `Stdio` at the application
* entry point.
*
* **Example** (Running commands with standard input)
*
* ```ts
* import { Console, Effect } from "effect"
* import { Command, Flag } from "effect/unstable/cli"
*
* const greetCommand = Command.make("greet", {
* name: Flag.string("name")
* }, (config) =>
* Effect.gen(function*() {
* yield* Console.log(`Hello, ${config.name}!`)
* }))
*
* // Automatically gets args from the Stdio service
* const program = Command.run(greetCommand, {
* version: "1.0.0"
* })
* ```
*
* @see {@link runWith} for running a command with an explicit argument array
*
* @category command execution
* @since 4.0.0
*/
export const run = /*#__PURE__*/dual(2, (command, config) => Stdio.Stdio.use(({
args
}) => Effect.flatMap(args, args => runWith(command, config)(args))));
/**
* Runs a command with explicitly provided arguments instead of using arguments from `Stdio`.
*
* **When to use**
*
* Use when you need to test CLI applications or programmatically execute
* commands with specific arguments.
*
* **Example** (Running commands with explicit arguments)
*
* ```ts
* import { Console, Effect } from "effect"
* import { Command, Flag } from "effect/unstable/cli"
*
* const greet = Command.make("greet", {
* name: Flag.string("name"),
* count: Flag.integer("count").pipe(Flag.withDefault(1))
* }, (config) =>
* Effect.gen(function*() {
* for (let i = 0; i < config.count; i++) {
* yield* Console.log(`Hello, ${config.name}!`)
* }
* }))
*
* // Test with specific arguments
* const testProgram = Effect.gen(function*() {
* const runCommand = Command.runWith(greet, { version: "1.0.0" })
*
* // Test normal execution
* yield* runCommand(["--name", "Alice", "--count", "2"])
*
* // Test help display
* yield* runCommand(["--help"])
*
* // Test version display
* yield* runCommand(["--version"])
* })
* ```
*
* @category command execution
* @since 4.0.0
*/
export const runWith = (command, config) => {
const commandImpl = toImpl(command);
return Effect.fnUntraced(function* (args) {
const {
tokens,
trailingOperands
} = Lexer.lex(args);
// 1. Collect known global flags from the command tree
const allFlags = getGlobalFlagsForCommandTree(command, GlobalFlag.BuiltIns);
// 2. Extract global flag tokens
const allFlagParams = allFlags.flatMap(f => Param.extractSingleParams(f.flag));
const globalRegistry = Parser.createFlagRegistry(allFlagParams.filter(Param.isFlagParam));
const {
flagMap,
remainder,
errors: globalFlagErrors
} = Parser.consumeGlobalFlags(tokens, command, globalRegistry);
const emptyArgs = {
flags: flagMap,
arguments: []
};
// 3. Parse command arguments from remaining tokens
const parsedArgs = yield* Parser.parseArgs({
tokens: remainder,
trailingOperands
}, command);
const commandPath = [command.name, ...Parser.getCommandPath(parsedArgs)];
const handlerCtx = {
command,
commandPath,
version: config.version
};
const activeFlags = getGlobalFlagsForCommandPath(command, commandPath, GlobalFlag.BuiltIns);
// 4. Reject globals that were passed outside the active command scope
const outOfScopeErrors = getOutOfScopeGlobalFlagErrors(allFlags, activeFlags, flagMap, commandPath);
if (outOfScopeErrors.length > 0 || globalFlagErrors.length > 0) {
const parseErrors = parsedArgs.errors ?? [];
return yield* new CliError.ShowHelp({
commandPath,
errors: [...globalFlagErrors, ...outOfScopeErrors, ...parseErrors]
});
}
// 5. Process action flags — first present action wins, then exit
for (const flag of activeFlags) {
if (flag._tag !== "Action") continue;
const singles = Param.extractSingleParams(flag.flag);
const hasEntry = singles.some(s => {
const entries = flagMap[s.name];
return entries !== undefined && entries.length > 0;
});
if (!hasEntry) continue;
const [, value] = yield* flag.flag.parse(emptyArgs);
yield* flag.run(value, handlerCtx);
return;
}
// 6. Handle parsing errors
if (parsedArgs.errors && parsedArgs.errors.length > 0) {
return yield* new CliError.ShowHelp({
commandPath,
errors: parsedArgs.errors
});
}
const parseResult = yield* Effect.result(commandImpl.parse(parsedArgs));
if (parseResult._tag === "Failure") {
return yield* new CliError.ShowHelp({
commandPath,
errors: [parseResult.failure]
});
}
// 7. Provide setting values
let program = commandImpl.handle(parseResult.success, [command.name]);
for (const flag of activeFlags) {
if (flag._tag !== "Setting") continue;
const [, value] = yield* flag.flag.parse(emptyArgs);
program = Effect.provideService(program, flag, value);
}
const [, logLevel] = yield* GlobalFlag.LogLevel.flag.parse(emptyArgs);
program = Effect.provideService(program, GlobalFlag.LogLevel, logLevel);
// 8. Apply built-in setting behavior
const services = Option.match(logLevel, {
onNone: () => Context.empty(),
onSome: level => Context.make(References.MinimumLogLevel, level)
});
// 9. Run command handler with composed context
yield* Effect.provideContext(program, services);
}, Effect.catchFilter(error => CliError.isCliError(error) && error._tag === "ShowHelp" ? Result.succeed(error) : Result.fail(error), error => Effect.andThen(showHelp(command, error), Effect.fail(error))), Effect.catchFilter(e => Terminal.isQuitError(e) ? Result.succeed(e) : Result.fail(e), _ => Effect.interrupt));
};
//# sourceMappingURL=Command.js.map

Xet Storage Details

Size:
26.2 kB
·
Xet hash:
2ef21c080829988c61cf5d548750a4049fdf1e66fd6570f977b9644d8307c0bb

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.