diff --git a/packages/cli/README.md b/packages/cli/README.md index 776722bb..752ef99b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4,17 +4,9 @@ Command-line interface for interacting with Bucket services. The CLI allows you features, authentication, and generate TypeScript types for your Bucket features. With this tool, you can streamline your feature flagging workflow directly from your terminal. -## Quick Start +## Usage -Get started quickly by running the CLI directly from your project's root directory: -initializing the CLI (if not already setup), creating a feature, and generate the types all at once. - -```bash -# Initialize CLI (if not already setup), create a feature, and generate types all at once -npx @bucketco/cli new -``` - -or install it locally +Get started by installing the CLI locally in your project: ```bash # npm @@ -24,7 +16,8 @@ npm install --save-dev @bucketco/cli yarn add --dev @bucketco/cli ``` -then +Then running the `new` command from your project's root directory, +initializing the CLI, creating a feature, and generating the types all at once: ```bash # npm @@ -34,30 +27,19 @@ npx bucket new yarn bucket new ``` -### Global installation - -You can also install the CLI globally adding the it to your PATH allowing you to use the shorthand `bucket`, -but we recommend installing it locally instead to better maintain the version. - -```bash -npm install -g @bucketco/cli - -bucket -``` - ### Individual commands Instead of running `new` you can call each step individually. ```bash # Initialize Bucket in your project (if not already setup) -bucket init +npx bucket init # Create a new feature -bucket features create "My Feature" +npx bucket features create "My Feature" # Generate TypeScript types for your features -bucket features types +npx bucket features types ``` ## Configuration @@ -77,11 +59,10 @@ Here's a comprehensive list of configuration options available in the `bucket.co "appId": "ap123456789", "typesOutput": [ { - "path": "gen/features.ts", + "path": "gen/features.d.ts", "format": "react" } - ], - "keyFormat": "camelCase" + ] } ``` @@ -92,7 +73,6 @@ Here's a comprehensive list of configuration options available in the `bucket.co | `apiUrl` | API URL for Bucket services (overrides baseUrl for API calls). | "https://app.bucket.co/api" | | `appId` | Your Bucket application ID. | Required | | `typesOutput` | Path(s) where TypeScript types will be generated. Can be a string or an array of objects with `path` and `format` properties. Available formats: `react` and `node`. | "gen/features.ts" with format "react" | -| `keyFormat` | Format for feature keys (options: custom, pascalCase, camelCase, snakeCaseUpper, snakeCaseLower, kebabCaseUpper, kebabCaseLower). | "custom" | You can override these settings using command-line options for individual commands. diff --git a/packages/cli/commands/apps.ts b/packages/cli/commands/apps.ts index 80ba729f..bba42458 100644 --- a/packages/cli/commands/apps.ts +++ b/packages/cli/commands/apps.ts @@ -12,7 +12,7 @@ export const listAppsAction = async () => { try { const apps = await listApps(); spinner.succeed(`Loaded apps from ${chalk.cyan(baseUrl)}.`); - console.table(apps); + console.table(apps.map(({ name, id, demo }) => ({ name, id, demo }))); } catch (error) { spinner.fail("Failed to list apps."); void handleError(error, "Apps List"); diff --git a/packages/cli/commands/features.ts b/packages/cli/commands/features.ts index 46e4a2fc..8b22160d 100644 --- a/packages/cli/commands/features.ts +++ b/packages/cli/commands/features.ts @@ -5,18 +5,24 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname, isAbsolute, join, relative } from "node:path"; import ora, { Ora } from "ora"; +import { App, getApp, getOrg } from "../services/bootstrap.js"; import { createFeature, Feature, listFeatures } from "../services/features.js"; import { configStore } from "../stores/config.js"; import { handleError, MissingAppIdError } from "../utils/errors.js"; -import { genFeatureKey, genTypes, KeyFormatPatterns } from "../utils/gen.js"; +import { + genFeatureKey, + genTypes, + indentLines, + KeyFormatPatterns, +} from "../utils/gen.js"; import { appIdOption, featureKeyOption, featureNameArgument, - keyFormatOption, typesFormatOption, typesOutOption, } from "../utils/options.js"; +import { baseUrlSuffix, featureUrl } from "../utils/path.js"; type CreateFeatureArgs = { key?: string; @@ -30,6 +36,11 @@ export const createFeatureAction = async ( let spinner: Ora | undefined; try { if (!appId) throw new MissingAppIdError(); + const org = getOrg(); + const app = getApp(appId); + console.log( + `Creating feature for app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`, + ); if (!name) { name = await input({ message: "New feature name:", @@ -38,7 +49,7 @@ export const createFeatureAction = async ( } if (!key) { - const keyFormat = configStore.getConfig("keyFormat") ?? "custom"; + const keyFormat = org.featureKeyFormat; key = await input({ message: "New feature key:", default: genFeatureKey(name, keyFormat), @@ -46,14 +57,17 @@ export const createFeatureAction = async ( }); } - spinner = ora( - `Creating feature for app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}...`, - ).start(); + spinner = ora(`Creating feature...`).start(); const feature = await createFeature(appId, name, key); - // todo: would like to link to feature here but we don't have the env id, only app id spinner.succeed( - `Created feature ${chalk.cyan(feature.name)} with key ${chalk.cyan(feature.key)} at ${chalk.cyan(baseUrl)}.`, + `Created feature ${chalk.cyan(feature.name)} with key ${chalk.cyan(feature.key)}:`, ); + const production = app.environments.find((e) => e.isProduction); + if (production) { + console.log( + indentLines(chalk.magenta(featureUrl(baseUrl, production, feature))), + ); + } } catch (error) { spinner?.fail("Feature creation failed."); void handleError(error, "Features Create"); @@ -66,12 +80,13 @@ export const listFeaturesAction = async () => { try { if (!appId) throw new MissingAppIdError(); + const app = getApp(appId); spinner = ora( - `Loading features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}...`, + `Loading features of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`, ).start(); const features = await listFeatures(appId); spinner.succeed( - `Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}.`, + `Loaded features of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`, ); console.table( features.map(({ key, name, stage }) => ({ @@ -92,16 +107,18 @@ export const generateTypesAction = async () => { let spinner: Ora | undefined; let features: Feature[] = []; + let app: App | undefined; try { if (!appId) throw new MissingAppIdError(); + app = getApp(appId); spinner = ora( - `Loading features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}...`, + `Loading features of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}...`, ).start(); features = await listFeatures(appId, { includeRemoteConfigs: true, }); spinner.succeed( - `Loaded features of app ${chalk.cyan(appId)} at ${chalk.cyan(baseUrl)}.`, + `Loaded features of app ${chalk.cyan(app.name)}${baseUrlSuffix(baseUrl)}.`, ); } catch (error) { spinner?.fail("Loading features failed."); @@ -127,7 +144,7 @@ export const generateTypesAction = async () => { ); } - spinner.succeed(`Generated types for ${chalk.cyan(appId)}.`); + spinner.succeed(`Generated types for app ${chalk.cyan(app.name)}.`); } catch (error) { spinner?.fail("Type generation failed."); void handleError(error, "Features Types"); @@ -143,7 +160,6 @@ export function registerFeatureCommands(cli: Command) { .command("create") .description("Create a new feature.") .addOption(appIdOption) - .addOption(keyFormatOption) .addOption(featureKeyOption) .addArgument(featureNameArgument) .action(createFeatureAction); @@ -164,10 +180,9 @@ export function registerFeatureCommands(cli: Command) { // Update the config with the cli override values featuresCommand.hook("preAction", (_, command) => { - const { appId, keyFormat, out, format } = command.opts(); + const { appId, out, format } = command.opts(); configStore.setConfig({ appId, - keyFormat, typesOutput: out ? [{ path: out, format: format || "react" }] : undefined, }); }); diff --git a/packages/cli/commands/init.ts b/packages/cli/commands/init.ts index 80160230..45ebb2af 100644 --- a/packages/cli/commands/init.ts +++ b/packages/cli/commands/init.ts @@ -63,9 +63,6 @@ export const initAction = async (args: InitArgs = {}) => { }); } - const keyFormat = - apps.find((app) => app.id === appId)?.featureKeyFormat ?? "custom"; - // Get types output path const typesOutput = await input({ message: "Where should we generate the types?", @@ -85,7 +82,6 @@ export const initAction = async (args: InitArgs = {}) => { // Update config configStore.setConfig({ appId, - keyFormat, typesOutput: [{ path: typesOutput, format: typesFormat }], }); diff --git a/packages/cli/commands/new.ts b/packages/cli/commands/new.ts index a8aa5fbe..80135869 100644 --- a/packages/cli/commands/new.ts +++ b/packages/cli/commands/new.ts @@ -7,7 +7,6 @@ import { appIdOption, featureKeyOption, featureNameArgument, - keyFormatOption, typesFormatOption, typesOutOption, } from "../utils/options.js"; @@ -38,7 +37,6 @@ export function registerNewCommand(cli: Command) { "Initialize the Bucket CLI, authenticates, and creates a new feature.", ) .addOption(appIdOption) - .addOption(keyFormatOption) .addOption(typesOutOption) .addOption(typesFormatOption) .addOption(featureKeyOption) @@ -47,10 +45,9 @@ export function registerNewCommand(cli: Command) { // Update the config with the cli override values cli.hook("preAction", (command) => { - const { appId, keyFormat, out, format } = command.opts(); + const { appId, out, format } = command.opts(); configStore.setConfig({ appId, - keyFormat, typesOutput: out ? [{ path: out, format: format || "react" }] : undefined, }); }); diff --git a/packages/cli/index.ts b/packages/cli/index.ts index cc39c135..43569836 100755 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -7,11 +7,19 @@ import { registerAuthCommands } from "./commands/auth.js"; import { registerFeatureCommands } from "./commands/features.js"; import { registerInitCommand } from "./commands/init.js"; import { registerNewCommand } from "./commands/new.js"; +import { bootstrap, getUser } from "./services/bootstrap.js"; import { authStore } from "./stores/auth.js"; import { configStore } from "./stores/config.js"; +import { handleError } from "./utils/errors.js"; import { apiUrlOption, baseUrlOption, debugOption } from "./utils/options.js"; import { stripTrailingSlash } from "./utils/path.js"; +type Options = { + debug?: boolean; + baseUrl?: string; + apiUrl?: string; +}; + async function main() { // Must load tokens and config before anything else await authStore.initialize(); @@ -23,9 +31,10 @@ async function main() { program.addOption(apiUrlOption); // Pre-action hook - program.hook("preAction", () => { - const { debug, baseUrl, apiUrl } = program.opts(); + program.hook("preAction", async () => { + const { debug, baseUrl, apiUrl } = program.opts(); const cleanedBaseUrl = stripTrailingSlash(baseUrl?.trim()); + // Set baseUrl and apiUrl in config store, will skip if undefined configStore.setConfig({ baseUrl: cleanedBaseUrl, apiUrl: @@ -33,11 +42,23 @@ async function main() { (cleanedBaseUrl && `${cleanedBaseUrl}/api`), }); + try { + // Load bootstrap data if not already loaded + await bootstrap(); + } catch (error) { + void handleError( + debug ? error : `Unable to reach ${configStore.getConfig("baseUrl")}`, + "Connect", + ); + } + if (debug) { console.debug(chalk.cyan("\nDebug mode enabled")); + const user = getUser(); + console.debug(`Logged in as ${chalk.cyan(user.name ?? user.email)}`); console.debug( "Reading config from", - chalk.green(configStore.getConfigPath()), + chalk.cyan(configStore.getConfigPath()), ); console.table(configStore.getConfig()); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 3123007a..27d8c139 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/cli", - "version": "0.2.2", + "version": "0.2.3", "packageManager": "yarn@4.1.1", "description": "CLI for Bucket service", "main": "./dist/index.js", @@ -50,12 +50,14 @@ "find-up": "^7.0.0", "json5": "^2.2.3", "open": "^10.1.0", - "ora": "^8.1.0" + "ora": "^8.1.0", + "slug": "^10.0.0" }, "devDependencies": { "@bucketco/eslint-config": "workspace:^", "@bucketco/tsconfig": "workspace:^", "@types/node": "^22.5.1", + "@types/slug": "^5.0.9", "eslint": "^9.21.0", "prettier": "^3.5.2", "shx": "^0.3.4", diff --git a/packages/cli/schema.json b/packages/cli/schema.json index fd5100f7..7d10c8f3 100644 --- a/packages/cli/schema.json +++ b/packages/cli/schema.json @@ -4,16 +4,24 @@ "type": "object", "properties": { "baseUrl": { - "type": "string" + "type": "string", + "pattern": "^https?://.*", + "description": "Base URL for the API. Defaults to https://app.bucket.co." }, "apiUrl": { - "type": "string" + "type": "string", + "pattern": "^https?://.*", + "description": "API URL for the API. Defaults to https://app.bucket.co/api." }, "appId": { - "type": "string" + "type": "string", + "minLength": 14, + "maxLength": 14, + "description": "Mandatory ID for the Bucket app. You can find it by calling bucket apps list." }, "typesOutput": { "type": "array", + "description": "List of paths to output the types. The path is relative to the current working directory.", "items": { "type": "object", "properties": { @@ -22,18 +30,6 @@ }, "required": ["path"] } - }, - "keyFormat": { - "type": "string", - "enum": [ - "custom", - "pascalCase", - "camelCase", - "snakeCaseUpper", - "snakeCaseLower", - "kebabCaseUpper", - "kebabCaseLower" - ] } }, "required": ["appId"] diff --git a/packages/cli/services/bootstrap.ts b/packages/cli/services/bootstrap.ts index f1f5d20a..7630087d 100644 --- a/packages/cli/services/bootstrap.ts +++ b/packages/cli/services/bootstrap.ts @@ -1,37 +1,100 @@ -import { KeyFormat } from "../stores/config.js"; import { authRequest } from "../utils/auth.js"; +import { KeyFormat } from "../utils/gen.js"; -type BootstrapResponse = { - org: { - apps: { - id: string; - name: string; - demo: boolean; - }[]; - featureKeyFormat?: KeyFormat; - }; +export type BootstrapResponse = { + org: Org; + user: User; +}; + +export type Org = { + id: string; + name: string; + logoUrl: string; + apps: App[]; + inviteKey: string; + createdAt: Date; + updatedAt: Date; + trialEndsAt: null; + suspendedAt: null; + accessLevel: string; + domain: null; + domainAutoJoin: boolean; + isGlobal: boolean; + featureKeyFormat: KeyFormat; +}; + +export type Environment = { + id: string; + name: string; + isProduction: boolean; + order: number; }; export type App = { id: string; name: string; demo: boolean; - featureKeyFormat: KeyFormat; + environments: Environment[]; +}; + +export type User = { + id: string; + email: string; + name: string; + createdAt: Date; + updatedAt: Date; + avatarUrl: string; + isAdmin: boolean; }; -export async function listApps(): Promise { - const response = await authRequest(`/bootstrap`); - const org = response.org; +let bootstrapResponse: BootstrapResponse | null = null; + +export async function bootstrap(): Promise { + if (!bootstrapResponse) { + bootstrapResponse = await authRequest(`/bootstrap`); + } + return bootstrapResponse; +} + +export function getOrg(): Org { + if (!bootstrapResponse) { + throw new Error("CLI has not been bootstrapped."); + } + if (!bootstrapResponse.org) { + throw new Error("No organization found."); + } + return bootstrapResponse.org; +} + +export function listApps(): App[] { + if (!bootstrapResponse) { + throw new Error("CLI has not been bootstrapped."); + } + const org = bootstrapResponse.org; if (!org) { - throw new Error("No organization found"); + throw new Error("No organization found."); } if (!org.apps?.length) { - throw new Error("No apps found"); - } - return response.org.apps.map(({ id, name, demo }) => ({ - name, - id, - featureKeyFormat: org.featureKeyFormat ?? "custom", - demo, - })); + throw new Error("No apps found."); + } + return bootstrapResponse.org.apps; +} + +export function getApp(id: string): App { + const apps = listApps(); + const app = apps.find((a) => a.id === id); + if (!app) { + throw new Error(`App with id ${id} not found`); + } + return app; +} + +export function getUser(): User { + if (!bootstrapResponse) { + throw new Error("CLI has not been bootstrapped."); + } + if (!bootstrapResponse.user) { + throw new Error("No user found."); + } + return bootstrapResponse.user; } diff --git a/packages/cli/services/features.ts b/packages/cli/services/features.ts index 6ff3baaf..1096b8ba 100644 --- a/packages/cli/services/features.ts +++ b/packages/cli/services/features.ts @@ -16,6 +16,7 @@ export type RemoteConfig = { }; export type Feature = { + id: string; name: string; key: string; remoteConfigs: RemoteConfig[]; diff --git a/packages/cli/stores/config.ts b/packages/cli/stores/config.ts index 21126ae5..8c8dead9 100644 --- a/packages/cli/stores/config.ts +++ b/packages/cli/stores/config.ts @@ -16,17 +16,6 @@ import { import { ConfigValidationError, handleError } from "../utils/errors.js"; import { stripTrailingSlash } from "../utils/path.js"; -export const keyFormats = [ - "custom", - "pascalCase", - "camelCase", - "snakeCaseUpper", - "snakeCaseLower", - "kebabCaseUpper", - "kebabCaseLower", -] as const; -export type KeyFormat = (typeof keyFormats)[number]; - export const typeFormats = ["react", "node"] as const; export type TypeFormat = (typeof typeFormats)[number]; @@ -41,7 +30,6 @@ type Config = { apiUrl: string; appId: string | undefined; typesOutput: TypesOutput[]; - keyFormat: KeyFormat; }; const defaultConfig: Config = { @@ -50,7 +38,6 @@ const defaultConfig: Config = { apiUrl: DEFAULT_API_URL, appId: undefined, typesOutput: [{ path: DEFAULT_TYPES_OUTPUT, format: "react" }], - keyFormat: "custom", }; // Helper to normalize typesOutput to array format diff --git a/packages/cli/utils/auth.ts b/packages/cli/utils/auth.ts index 7f4d75e7..bd7217d7 100644 --- a/packages/cli/utils/auth.ts +++ b/packages/cli/utils/auth.ts @@ -4,7 +4,7 @@ import open from "open"; import { authStore } from "../stores/auth.js"; import { configStore } from "../stores/config.js"; -import { loginUrl } from "./constants.js"; +import { loginUrl } from "./path.js"; function corsHeaders(baseUrl: string): Record { return { diff --git a/packages/cli/utils/constants.ts b/packages/cli/utils/constants.ts index bed1a97d..844d4174 100644 --- a/packages/cli/utils/constants.ts +++ b/packages/cli/utils/constants.ts @@ -10,7 +10,3 @@ export const SCHEMA_URL = `https://unpkg.com/@bucketco/cli@latest/schema.json`; export const DEFAULT_BASE_URL = "https://app.bucket.co"; export const DEFAULT_API_URL = `${DEFAULT_BASE_URL}/api`; export const DEFAULT_TYPES_OUTPUT = join("gen", "features.d.ts"); - -export const loginUrl = (baseUrl: string, localPort: number) => - `${baseUrl}/login?redirect_url=` + - encodeURIComponent("/cli-login?port=" + localPort); diff --git a/packages/cli/utils/errors.ts b/packages/cli/utils/errors.ts index b70f2c78..c0df8ce2 100644 --- a/packages/cli/utils/errors.ts +++ b/packages/cli/utils/errors.ts @@ -47,6 +47,7 @@ export async function handleError(error: unknown, tag: string) { } } else if (error instanceof Error) { console.error(chalk.red(tag, error.message)); + if (error.cause) console.error(error.cause); } else if (typeof error === "string") { console.error(chalk.red(tag, error)); } else { diff --git a/packages/cli/utils/gen.ts b/packages/cli/utils/gen.ts index 88363980..d02f21ce 100644 --- a/packages/cli/utils/gen.ts +++ b/packages/cli/utils/gen.ts @@ -69,7 +69,7 @@ export const KeyFormatPatterns: Record = { }, }; -function indentLines(str: string, indent = 2, lineBreak = "\n"): string { +export function indentLines(str: string, indent = 2, lineBreak = "\n"): string { const indentStr = " ".repeat(indent); return str .split(lineBreak) diff --git a/packages/cli/utils/options.ts b/packages/cli/utils/options.ts index 92046ab4..912b9459 100644 --- a/packages/cli/utils/options.ts +++ b/packages/cli/utils/options.ts @@ -1,7 +1,5 @@ import { Argument, Option } from "commander"; -import { keyFormats } from "../stores/config.js"; - import { CONFIG_FILE_NAME } from "./constants.js"; export const debugOption = new Option("--debug", "Enable debug mode"); @@ -36,11 +34,6 @@ export const typesFormatOption = new Option( "Single output format for generated feature types", ).choices(["react", "node"]); -export const keyFormatOption = new Option( - "--key-format [format]", - `Feature key format. Falls back to keyFormat value in ${CONFIG_FILE_NAME}.`, -).choices(keyFormats); - export const featureKeyOption = new Option( "-k, --key [feature key]", "Feature key. If not provided, a key is generated from the feature's name.", diff --git a/packages/cli/utils/path.ts b/packages/cli/utils/path.ts index a608cf1d..5bade673 100644 --- a/packages/cli/utils/path.ts +++ b/packages/cli/utils/path.ts @@ -1,3 +1,34 @@ +import chalk from "chalk"; +import slugMod from "slug"; + +import { DEFAULT_BASE_URL } from "./constants.js"; + +export type UrlArgs = { id: string; name: string }; + +export function slug({ id, name }: UrlArgs) { + return `${slugMod(name).substring(0, 15)}-${id}`; +} + export function stripTrailingSlash(str: T): T { return str?.endsWith("/") ? (str.slice(0, -1) as T) : str; } + +export const baseUrlSuffix = (baseUrl: string) => { + return baseUrl !== DEFAULT_BASE_URL ? ` at ${chalk.cyan(baseUrl)}` : ""; +}; + +export const loginUrl = (baseUrl: string, localPort: number) => + `${baseUrl}/login?redirect_url=` + + encodeURIComponent("/cli-login?port=" + localPort); + +export function environmentUrl(baseUrl: string, environment: UrlArgs): string { + return `${baseUrl}/envs/${slug(environment)}`; +} + +export function featureUrl( + baseUrl: string, + env: UrlArgs, + feature: UrlArgs, +): string { + return `${environmentUrl(baseUrl, env)}/features/${slug(feature)}`; +} diff --git a/yarn.lock b/yarn.lock index 5150e84c..ba547b9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -456,6 +456,7 @@ __metadata: "@bucketco/tsconfig": "workspace:^" "@inquirer/prompts": "npm:^5.3.8" "@types/node": "npm:^22.5.1" + "@types/slug": "npm:^5.0.9" ajv: "npm:^8.17.1" chalk: "npm:^5.3.0" change-case: "npm:^5.4.4" @@ -468,6 +469,7 @@ __metadata: ora: "npm:^8.1.0" prettier: "npm:^3.5.2" shx: "npm:^0.3.4" + slug: "npm:^10.0.0" typescript: "npm:^5.5.4" vitest: "npm:^3.0.8" bin: @@ -3823,6 +3825,13 @@ __metadata: languageName: node linkType: hard +"@types/slug@npm:^5.0.9": + version: 5.0.9 + resolution: "@types/slug@npm:5.0.9" + checksum: 10c0/6d5366d80d83a8d08b7d33ea14394511997de2d6001d1e463a5141aa10bf0dd1ec801e0e248b8a84239142064f96918638db7ca2c745c85891d6c34c309ad3a6 + languageName: node + linkType: hard + "@types/statuses@npm:^2.0.4": version: 2.0.5 resolution: "@types/statuses@npm:2.0.5" @@ -15226,6 +15235,15 @@ __metadata: languageName: node linkType: hard +"slug@npm:^10.0.0": + version: 10.0.0 + resolution: "slug@npm:10.0.0" + bin: + slug: cli.js + checksum: 10c0/330f7657dd44e526412417cb3225faf88fe5190ebbf1d9345b572d8e84358efa7b25412447aa445f84102e763d9ce1bbe79331aa0e2162ddf0324ae02cb6d3e4 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0"