Merge pull request #33036 from backstage/rugvip/cli-execute-loader
cli: add lazy loader pattern for command execution
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Internal refactor of CLI modularization, moving individual commands to be implemented with cleye.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/repo-tools': patch
|
||||
---
|
||||
|
||||
Updated the CLI report parser to support cleye-style help output, and strip ANSI escape codes from captured output.
|
||||
@@ -577,3 +577,4 @@ zsh
|
||||
resizable
|
||||
enums
|
||||
LLMs
|
||||
cleye
|
||||
|
||||
+42
-51
@@ -57,81 +57,75 @@ Commands:
|
||||
### `backstage-cli config docs`
|
||||
|
||||
```
|
||||
Usage: backstage-cli config docs [options]
|
||||
Usage: backstage-cli config docs
|
||||
|
||||
Options:
|
||||
--package <name>
|
||||
--package <string>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli config schema`
|
||||
|
||||
```
|
||||
Usage: <none>
|
||||
Usage: backstage-cli config schema
|
||||
|
||||
Options:
|
||||
--format
|
||||
--help
|
||||
--format <string>
|
||||
--merge
|
||||
--no-merge
|
||||
--package
|
||||
--version
|
||||
--package <string>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli config:check`
|
||||
|
||||
```
|
||||
Usage: <none>
|
||||
Usage: backstage-cli config:check
|
||||
|
||||
Options:
|
||||
--config
|
||||
--config <string>
|
||||
--deprecated
|
||||
--frontend
|
||||
--help
|
||||
--lax
|
||||
--package
|
||||
--package <string>
|
||||
--strict
|
||||
--version
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli config:docs`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli config:docs
|
||||
|
||||
Options:
|
||||
--package <name>
|
||||
--package <string>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli config:print`
|
||||
|
||||
```
|
||||
Usage: <none>
|
||||
Usage: backstage-cli config:print
|
||||
|
||||
Options:
|
||||
--config
|
||||
--format
|
||||
--config <string>
|
||||
--format <string>
|
||||
--frontend
|
||||
--help
|
||||
--lax
|
||||
--package
|
||||
--version
|
||||
--package <string>
|
||||
--with-secrets
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli config:schema`
|
||||
|
||||
```
|
||||
Usage: <none>
|
||||
Usage: backstage-cli config:schema
|
||||
|
||||
Options:
|
||||
--format
|
||||
--help
|
||||
--format <string>
|
||||
--merge
|
||||
--no-merge
|
||||
--package
|
||||
--version
|
||||
--package <string>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli create-github-app`
|
||||
@@ -146,13 +140,12 @@ Options:
|
||||
### `backstage-cli info`
|
||||
|
||||
```
|
||||
Usage: <none>
|
||||
Usage: backstage-cli info
|
||||
|
||||
Options:
|
||||
--format
|
||||
--help
|
||||
--include
|
||||
--version
|
||||
--format <string>
|
||||
--include <string>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli migrate`
|
||||
@@ -175,7 +168,7 @@ Commands:
|
||||
### `backstage-cli migrate package-exports`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli migrate package-exports
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -184,7 +177,7 @@ Options:
|
||||
### `backstage-cli migrate package-lint-configs`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli migrate package-lint-configs
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -193,7 +186,7 @@ Options:
|
||||
### `backstage-cli migrate package-roles`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli migrate package-roles
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -202,7 +195,7 @@ Options:
|
||||
### `backstage-cli migrate package-scripts`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli migrate package-scripts
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -211,7 +204,7 @@ Options:
|
||||
### `backstage-cli migrate react-router-deps`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli migrate react-router-deps
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -271,7 +264,7 @@ Options:
|
||||
### `backstage-cli package clean`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli package clean
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -293,7 +286,7 @@ Options:
|
||||
### `backstage-cli package postpack`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli package postpack
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -302,7 +295,7 @@ Options:
|
||||
### `backstage-cli package prepack`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli package prepack
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -472,7 +465,7 @@ Options:
|
||||
### `backstage-cli repo clean`
|
||||
|
||||
```
|
||||
Usage: program [options]
|
||||
Usage: backstage-cli repo clean
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
@@ -560,25 +553,23 @@ Commands:
|
||||
### `backstage-cli translations export`
|
||||
|
||||
```
|
||||
Usage: <none>
|
||||
Usage: backstage-cli translations export
|
||||
|
||||
Options:
|
||||
--help
|
||||
--output
|
||||
--pattern
|
||||
--version
|
||||
--output <string>
|
||||
--pattern <string>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli translations import`
|
||||
|
||||
```
|
||||
Usage: <none>
|
||||
Usage: backstage-cli translations import
|
||||
|
||||
Options:
|
||||
--help
|
||||
--input
|
||||
--output
|
||||
--version
|
||||
--input <string>
|
||||
--output <string>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli versions:bump`
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"chalk": "^4.0.0",
|
||||
"chokidar": "^3.3.1",
|
||||
"cleye": "^2.2.1",
|
||||
"commander": "^14.0.3",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
export default async function clean() {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
await fs.remove(targetPaths.resolve('dist'));
|
||||
await fs.remove(targetPaths.resolve('dist-types'));
|
||||
await fs.remove(targetPaths.resolve('coverage'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
|
||||
import { run, targetPaths } from '@backstage/cli-common';
|
||||
import type { CommandContext } from '../../../../wiring/types';
|
||||
|
||||
export async function command(): Promise<void> {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await fs.remove(targetPaths.resolveRoot('dist'));
|
||||
@@ -48,4 +50,4 @@ export async function command(): Promise<void> {
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { Command, Option } from 'commander';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
@@ -206,52 +207,36 @@ export const buildPlugin = createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['package', 'clean'],
|
||||
description: 'Delete cache directories',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/clean'), 'default'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/package/clean'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'prepack'],
|
||||
description: 'Prepares a package for packaging before publishing',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/pack'), 'pre'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: async ({ args, info }) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
const { pre } = await import('./commands/package/pack');
|
||||
await pre();
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['package', 'postpack'],
|
||||
description: 'Restores the changes made by the prepack command',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/package/pack'), 'post'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: async ({ args, info }) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
const { post } = await import('./commands/package/pack');
|
||||
await post();
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['repo', 'clean'],
|
||||
description: 'Delete cache and output directories',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/repo/clean'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/repo/clean'),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -14,20 +14,38 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { mergeConfigSchemas } from '@backstage/config-loader';
|
||||
import { OptionValues } from 'commander';
|
||||
import { JSONSchema7 as JSONSchema } from 'json-schema';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
import chalk from 'chalk';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
const DOCS_URL = 'https://config.backstage.io';
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
package: {
|
||||
type: String,
|
||||
description:
|
||||
'Only include the schema that applies to the given package',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const { schema: appSchemas } = await loadCliConfig({
|
||||
args: [],
|
||||
fromPackage: opts.package,
|
||||
fromPackage: pkg,
|
||||
mockEnv: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -14,36 +14,67 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import { AppConfig, ConfigReader } from '@backstage/config';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import { ConfigSchema, ConfigVisibility } from '@backstage/config-loader';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { config, lax, frontend, withSecrets, format, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
package: { type: String, description: 'Package to print config for' },
|
||||
lax: {
|
||||
type: Boolean,
|
||||
description: 'Do not require environment variables to be set',
|
||||
},
|
||||
frontend: { type: Boolean, description: 'Only print frontend config' },
|
||||
withSecrets: {
|
||||
type: Boolean,
|
||||
description: 'Include secrets in the output',
|
||||
},
|
||||
format: { type: String, description: 'Output format (yaml or json)' },
|
||||
config: {
|
||||
type: [String],
|
||||
description: 'Config files to load instead of app-config.yaml',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
const { schema, appConfigs } = await loadCliConfig({
|
||||
args: opts.config,
|
||||
fromPackage: opts.package,
|
||||
mockEnv: opts.lax,
|
||||
fullVisibility: !opts.frontend,
|
||||
args: config,
|
||||
fromPackage: pkg,
|
||||
mockEnv: lax,
|
||||
fullVisibility: !frontend,
|
||||
});
|
||||
const visibility = getVisibilityOption(opts);
|
||||
const visibility = getVisibilityOption(frontend, withSecrets);
|
||||
const data = serializeConfigData(appConfigs, schema, visibility);
|
||||
|
||||
if (opts.format === 'json') {
|
||||
if (format === 'json') {
|
||||
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${stringifyYaml(data)}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
function getVisibilityOption(opts: OptionValues): ConfigVisibility {
|
||||
if (opts.frontend && opts.withSecrets) {
|
||||
function getVisibilityOption(
|
||||
frontend: boolean | undefined,
|
||||
withSecrets: boolean | undefined,
|
||||
): ConfigVisibility {
|
||||
if (frontend && withSecrets) {
|
||||
throw new Error('Not allowed to combine frontend and secret config');
|
||||
}
|
||||
if (opts.frontend) {
|
||||
if (frontend) {
|
||||
return 'frontend';
|
||||
} else if (opts.withSecrets) {
|
||||
} else if (withSecrets) {
|
||||
return 'secret';
|
||||
}
|
||||
return 'backend';
|
||||
|
||||
@@ -14,22 +14,41 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { JSONSchema7 as JSONSchema } from 'json-schema';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { mergeConfigSchemas } from '@backstage/config-loader';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { merge, format, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
package: { type: String, description: 'Package to print schema for' },
|
||||
format: { type: String, description: 'Output format (yaml or json)' },
|
||||
merge: {
|
||||
type: Boolean,
|
||||
description: 'Merge all schemas into a single schema',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
const { schema } = await loadCliConfig({
|
||||
args: [],
|
||||
fromPackage: opts.package,
|
||||
fromPackage: pkg,
|
||||
mockEnv: true,
|
||||
});
|
||||
|
||||
let configSchema: JsonObject | JSONSchema;
|
||||
if (opts.merge) {
|
||||
if (merge) {
|
||||
configSchema = mergeConfigSchemas(
|
||||
(schema.serialize().schemas as JsonObject[]).map(
|
||||
_ => _.value as JSONSchema,
|
||||
@@ -42,7 +61,7 @@ export default async (opts: OptionValues) => {
|
||||
configSchema = schema.serialize();
|
||||
}
|
||||
|
||||
if (opts.format === 'json') {
|
||||
if (format === 'json') {
|
||||
process.stdout.write(`${JSON.stringify(configSchema, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${stringifyYaml(configSchema)}\n`);
|
||||
|
||||
@@ -14,16 +14,47 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionValues } from 'commander';
|
||||
import { cli } from 'cleye';
|
||||
import { loadCliConfig } from '../lib/config';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { config, lax, frontend, deprecated, strict, package: pkg },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
package: {
|
||||
type: String,
|
||||
description: 'Package to validate config for',
|
||||
},
|
||||
lax: {
|
||||
type: Boolean,
|
||||
description: 'Do not require environment variables to be set',
|
||||
},
|
||||
frontend: {
|
||||
type: Boolean,
|
||||
description: 'Only validate frontend config',
|
||||
},
|
||||
deprecated: { type: Boolean, description: 'Output deprecated keys' },
|
||||
strict: { type: Boolean, description: 'Enable strict validation' },
|
||||
config: {
|
||||
type: [String],
|
||||
description: 'Config files to load instead of app-config.yaml',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
export default async (opts: OptionValues) => {
|
||||
await loadCliConfig({
|
||||
args: opts.config,
|
||||
fromPackage: opts.package,
|
||||
mockEnv: opts.lax,
|
||||
fullVisibility: !opts.frontend,
|
||||
withDeprecatedKeys: opts.deprecated,
|
||||
strict: opts.strict,
|
||||
args: config,
|
||||
fromPackage: pkg,
|
||||
mockEnv: lax,
|
||||
fullVisibility: !frontend,
|
||||
withDeprecatedKeys: deprecated,
|
||||
strict,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import yargs from 'yargs';
|
||||
import { Command } from 'commander';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export const configOption = [
|
||||
'--config <path>',
|
||||
@@ -31,108 +28,33 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['config:docs'],
|
||||
description: 'Browse the configuration reference documentation',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command
|
||||
.option(
|
||||
'--package <name>',
|
||||
'Only include the schema that applies to the given package',
|
||||
)
|
||||
.description('Browse the configuration reference documentation')
|
||||
.action(lazy(() => import('./commands/docs'), 'default'));
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/docs') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config', 'docs'],
|
||||
description: 'Browse the configuration reference documentation',
|
||||
execute: async ({ args, info }) => {
|
||||
await new Command(info.usage)
|
||||
.option(
|
||||
'--package <name>',
|
||||
'Only include the schema that applies to the given package',
|
||||
)
|
||||
.description(info.description)
|
||||
.action(lazy(() => import('./commands/docs'), 'default'))
|
||||
.parseAsync(args, { from: 'user' });
|
||||
},
|
||||
execute: { loader: () => import('./commands/docs') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:print'],
|
||||
description: 'Print the app configuration for the current package',
|
||||
execute: async ({ args, info }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
lax: { type: 'boolean' },
|
||||
frontend: { type: 'boolean' },
|
||||
'with-secrets': { type: 'boolean' },
|
||||
format: { type: 'string' },
|
||||
config: { type: 'string', array: true, default: [] },
|
||||
})
|
||||
.usage('$0', info.description)
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/print'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/print') },
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:check'],
|
||||
description:
|
||||
'Validate that the given configuration loads and matches schema',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
lax: { type: 'boolean' },
|
||||
frontend: { type: 'boolean' },
|
||||
deprecated: { type: 'boolean' },
|
||||
strict: { type: 'boolean' },
|
||||
config: {
|
||||
type: 'string',
|
||||
array: true,
|
||||
default: [],
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/validate'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/validate') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['config:schema'],
|
||||
description: 'Print the JSON schema for the given configuration',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
format: { type: 'string' },
|
||||
merge: { type: 'boolean' },
|
||||
'no-merge': { type: 'boolean' },
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/schema'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/schema') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['config', 'schema'],
|
||||
description: 'Print the JSON schema for the given configuration',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
format: { type: 'string' },
|
||||
merge: { type: 'boolean' },
|
||||
'no-merge': { type: 'boolean' },
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/schema'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/schema') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { version as cliVersion } from '../../../../package.json';
|
||||
import os from 'node:os';
|
||||
import { runOutput, targetPaths, findOwnPaths } from '@backstage/cli-common';
|
||||
@@ -24,11 +25,7 @@ import {
|
||||
} from '@backstage/cli-node';
|
||||
import { minimatch } from 'minimatch';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
interface InfoOptions {
|
||||
include: string[];
|
||||
format: 'text' | 'json';
|
||||
}
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
/**
|
||||
* Attempts to read package.json from node_modules for a given package name.
|
||||
@@ -55,7 +52,31 @@ function hasBackstageField(packageName: string, targetPath: string): boolean {
|
||||
return pkg?.backstage !== undefined;
|
||||
}
|
||||
|
||||
export default async (options: InfoOptions) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { include, format },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
include: {
|
||||
type: [String],
|
||||
description:
|
||||
'Glob patterns for additional packages to include (e.g., @spotify/backstage*)',
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
description: 'Output format (text or json)',
|
||||
default: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const options = { include, format: format as 'text' | 'json' };
|
||||
|
||||
await new Promise(async () => {
|
||||
const yarnVersion = await runOutput(['yarn', '--version']);
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import yargs from 'yargs';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'info',
|
||||
@@ -23,27 +21,7 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['info'],
|
||||
description: 'Show helpful information for debugging and reporting bugs',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
include: {
|
||||
type: 'string',
|
||||
array: true,
|
||||
default: [],
|
||||
description:
|
||||
'Glob patterns for additional packages to include (e.g., @spotify/backstage*)',
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
choices: ['text', 'json'],
|
||||
default: 'text',
|
||||
description: 'Output format (text or json)',
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/info'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/info') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export async function command() {
|
||||
import { cli } from 'cleye';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
throw new Error(
|
||||
'The `migrate package-exports` command has been removed, use `repo fix` instead.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,14 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
import { runOutput } from '@backstage/cli-common';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`;
|
||||
|
||||
export async function command() {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const oldConfigs = [
|
||||
@@ -86,4 +89,4 @@ export async function command() {
|
||||
if (hasPrettier) {
|
||||
await runOutput(['prettier', '--write', ...configPaths]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,13 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { getPackages } from '@manypkg/get-packages';
|
||||
import { PackageRoles } from '@backstage/cli-node';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
export default async () => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
const { packages } = await getPackages(targetPaths.dir);
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -14,15 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { PackageGraph, PackageRoles, PackageRole } from '@backstage/cli-node';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
const configArgPattern = /--config[=\s][^\s$]+/;
|
||||
|
||||
const noStartRoles: PackageRole[] = ['cli', 'common-library'];
|
||||
|
||||
export async function command() {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await Promise.all(
|
||||
@@ -56,14 +59,14 @@ export async function command() {
|
||||
// For test scripts we keep all existing flags except for --passWithNoTests, since that's now default
|
||||
const testCmd = ['test'];
|
||||
if (scripts.test?.startsWith('backstage-cli test')) {
|
||||
const args = scripts.test
|
||||
const testArgs = scripts.test
|
||||
.slice('backstage-cli test'.length)
|
||||
.split(' ')
|
||||
.filter(Boolean);
|
||||
if (args.includes('--passWithNoTests')) {
|
||||
args.splice(args.indexOf('--passWithNoTests'), 1);
|
||||
if (testArgs.includes('--passWithNoTests')) {
|
||||
testArgs.splice(testArgs.indexOf('--passWithNoTests'), 1);
|
||||
}
|
||||
testCmd.push(...args);
|
||||
testCmd.push(...testArgs);
|
||||
}
|
||||
|
||||
const expectedScripts = {
|
||||
@@ -104,4 +107,4 @@ export async function command() {
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,14 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { PackageGraph, PackageRoles } from '@backstage/cli-node';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
const REACT_ROUTER_DEPS = ['react-router', 'react-router-dom'];
|
||||
const REACT_ROUTER_RANGE = '6.0.0-beta.0 || ^6.3.0';
|
||||
|
||||
export async function command() {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
cli({ help: info }, undefined, args);
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
await Promise.all(
|
||||
@@ -56,4 +59,4 @@ export async function command() {
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,39 +68,24 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-roles'],
|
||||
description: `Add package role field to packages that don't have it`,
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/packageRole'), 'default'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/packageRole'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-scripts'],
|
||||
description: 'Set package scripts according to each package role',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/packageScripts'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/packageScripts'),
|
||||
},
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['migrate', 'package-exports'],
|
||||
description: 'Synchronize package subpath export definitions',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/packageExports'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/packageExports'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -108,13 +93,8 @@ export default createCliPlugin({
|
||||
path: ['migrate', 'package-lint-configs'],
|
||||
description:
|
||||
'Migrates all packages to use @backstage/cli/config/eslint-factory',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/packageLintConfigs'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/packageLintConfigs'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,13 +102,8 @@ export default createCliPlugin({
|
||||
path: ['migrate', 'react-router-deps'],
|
||||
description:
|
||||
'Migrates the react-router dependencies for all packages to be peer dependencies',
|
||||
execute: async ({ args }) => {
|
||||
const command = new Command();
|
||||
const defaultCommand = command.action(
|
||||
lazy(() => import('./commands/reactRouterDeps'), 'command'),
|
||||
);
|
||||
|
||||
await defaultCommand.parseAsync(args, { from: 'user' });
|
||||
execute: {
|
||||
loader: () => import('./commands/reactRouterDeps'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -87,11 +87,6 @@ export default async (_opts: OptionValues, cmd: Command) => {
|
||||
}--no-node-snapshot`;
|
||||
}
|
||||
|
||||
// This ensures that the process doesn't exit too early before stdout is flushed
|
||||
if (args.includes('--help')) {
|
||||
(process.stdout as any)._handle.setBlocking(true);
|
||||
}
|
||||
|
||||
// Because of the ongoing migration to v30 of jest, jest is no longer hard-depended to allow
|
||||
// opt-in migration. Users instead need to add jest as a devDependency themselves and specify
|
||||
// the version they want. This prints a helpful error message if jest is not found, i.e. they
|
||||
|
||||
@@ -304,11 +304,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
|
||||
}--no-node-snapshot`;
|
||||
}
|
||||
|
||||
// This ensures that the process doesn't exit too early before stdout is flushed
|
||||
if (args.includes('--jest-help')) {
|
||||
removeOptionArg(args, '--jest-help');
|
||||
args.push('--help');
|
||||
(process.stdout as any)._handle.setBlocking(true);
|
||||
}
|
||||
|
||||
// This code path is enabled by the --successCache flag, which is specific to
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve as resolvePath } from 'node:path';
|
||||
@@ -28,16 +29,38 @@ import {
|
||||
} from '../lib/extractTranslations';
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_MESSAGE_PATTERN,
|
||||
formatMessagePath,
|
||||
validatePattern,
|
||||
} from '../lib/messageFilePath';
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
interface ExportOptions {
|
||||
output: string;
|
||||
pattern: string;
|
||||
}
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { output, pattern },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
output: {
|
||||
type: String,
|
||||
default: 'translations',
|
||||
description: 'Output directory for exported messages and manifest',
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
default: DEFAULT_MESSAGE_PATTERN,
|
||||
description:
|
||||
'File path pattern for message files, with {id} and {lang} placeholders',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const options = { output, pattern };
|
||||
|
||||
export default async (options: ExportOptions) => {
|
||||
validatePattern(options.pattern);
|
||||
|
||||
const targetPackageJson = await readTargetPackage(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import { targetPaths } from '@backstage/cli-common';
|
||||
import fs from 'fs-extra';
|
||||
import {
|
||||
@@ -27,11 +28,7 @@ import {
|
||||
createMessagePathParser,
|
||||
formatMessagePath,
|
||||
} from '../lib/messageFilePath';
|
||||
|
||||
interface ImportOptions {
|
||||
input: string;
|
||||
output: string;
|
||||
}
|
||||
import type { CommandContext } from '../../../wiring/types';
|
||||
|
||||
interface ManifestRefEntry {
|
||||
package: string;
|
||||
@@ -44,7 +41,31 @@ interface Manifest {
|
||||
refs: Record<string, ManifestRefEntry>;
|
||||
}
|
||||
|
||||
export default async (options: ImportOptions) => {
|
||||
export default async ({ args, info }: CommandContext) => {
|
||||
const {
|
||||
flags: { input, output },
|
||||
} = cli(
|
||||
{
|
||||
help: info,
|
||||
flags: {
|
||||
input: {
|
||||
type: String,
|
||||
default: 'translations',
|
||||
description:
|
||||
'Input directory containing the manifest and translated message files',
|
||||
},
|
||||
output: {
|
||||
type: String,
|
||||
default: 'src/translations/resources.ts',
|
||||
description: 'Output path for the generated wiring module',
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
args,
|
||||
);
|
||||
|
||||
const options = { input, output };
|
||||
await readTargetPackage(targetPaths.dir, targetPaths.rootDir);
|
||||
|
||||
const inputDir = resolvePath(targetPaths.dir, options.input);
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import yargs from 'yargs';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import { lazy } from '../../wiring/lazy';
|
||||
import { DEFAULT_MESSAGE_PATTERN } from './lib/messageFilePath';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'translations',
|
||||
@@ -25,51 +22,14 @@ export default createCliPlugin({
|
||||
path: ['translations', 'export'],
|
||||
description:
|
||||
'Export translation messages from an app and all of its frontend plugins to JSON files',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
output: {
|
||||
type: 'string',
|
||||
default: 'translations',
|
||||
description:
|
||||
'Output directory for exported messages and manifest',
|
||||
},
|
||||
pattern: {
|
||||
type: 'string',
|
||||
default: DEFAULT_MESSAGE_PATTERN,
|
||||
description:
|
||||
'File path pattern for message files, with {id} and {lang} placeholders',
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/export'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/export') },
|
||||
});
|
||||
|
||||
reg.addCommand({
|
||||
path: ['translations', 'import'],
|
||||
description:
|
||||
'Generate translation resource wiring from translated JSON files',
|
||||
execute: async ({ args }) => {
|
||||
const argv = await yargs()
|
||||
.options({
|
||||
input: {
|
||||
type: 'string',
|
||||
default: 'translations',
|
||||
description:
|
||||
'Input directory containing the manifest and translated message files',
|
||||
},
|
||||
output: {
|
||||
type: 'string',
|
||||
default: 'src/translations/resources.ts',
|
||||
description: 'Output path for the generated wiring module',
|
||||
},
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
await lazy(() => import('./commands/import'), 'default')(argv);
|
||||
},
|
||||
execute: { loader: () => import('./commands/import') },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,6 +67,31 @@ describe('CliInitializer', () => {
|
||||
expect(process.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should run commands using a loader', async () => {
|
||||
expect.assertions(2);
|
||||
process.argv = ['node', 'cli', 'test', '--verbose'];
|
||||
const initializer = new CliInitializer();
|
||||
initializer.add(
|
||||
createCliPlugin({
|
||||
pluginId: 'test',
|
||||
init: async reg =>
|
||||
reg.addCommand({
|
||||
path: ['test'],
|
||||
description: 'test',
|
||||
execute: {
|
||||
loader: async () => ({
|
||||
default: async ({ args }) => {
|
||||
expect(args).toEqual(['--verbose']);
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await initializer.run();
|
||||
expect(process.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should pass positional args to the subcommand if nested', async () => {
|
||||
expect.assertions(2);
|
||||
process.argv = [
|
||||
|
||||
@@ -118,13 +118,25 @@ export class CliInitializer {
|
||||
}
|
||||
positionalArgs.push(nonProcessArgs[argIndex]);
|
||||
}
|
||||
await node.command.execute({
|
||||
const context = {
|
||||
args: [...positionalArgs, ...args.unknown],
|
||||
info: {
|
||||
usage: [programName, ...node.command.path].join(' '),
|
||||
description: node.command.description,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof node.command.execute === 'function') {
|
||||
await node.command.execute(context);
|
||||
} else {
|
||||
const mod = await node.command.execute.loader();
|
||||
// Handle CJS double-wrapping of default exports
|
||||
const fn =
|
||||
typeof mod.default === 'function'
|
||||
? mod.default
|
||||
: (mod.default as any).default;
|
||||
await fn(context);
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (error: unknown) {
|
||||
exitWithError(error);
|
||||
@@ -144,7 +156,7 @@ export class CliInitializer {
|
||||
exitWithError(new ForwardedError('Unhandled rejection', rejection));
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
await program.parseAsync(process.argv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,23 +15,31 @@
|
||||
*/
|
||||
import { OpaqueType } from '@internal/opaque';
|
||||
|
||||
export interface CommandContext {
|
||||
args: string[];
|
||||
info: {
|
||||
/**
|
||||
* The usage string of the current command, for example: "backstage-cli repo test"
|
||||
*/
|
||||
usage: string;
|
||||
/**
|
||||
* The description provided for the command
|
||||
*/
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type CommandExecuteFn = (context: CommandContext) => Promise<void>;
|
||||
|
||||
export interface BackstageCommand {
|
||||
path: string[];
|
||||
description: string;
|
||||
deprecated?: boolean;
|
||||
execute: (context: {
|
||||
args: string[];
|
||||
info: {
|
||||
/**
|
||||
* The usage string of the current command, for example: "backstage-cli repo test"
|
||||
*/
|
||||
usage: string;
|
||||
/**
|
||||
* The description provided for the command
|
||||
*/
|
||||
description: string;
|
||||
};
|
||||
}) => Promise<void>;
|
||||
execute:
|
||||
| CommandExecuteFn
|
||||
| {
|
||||
loader: () => Promise<{ default: CommandExecuteFn }>;
|
||||
};
|
||||
}
|
||||
|
||||
export type CliFeature = CliPlugin;
|
||||
|
||||
@@ -27,7 +27,14 @@ import { generateCliReport } from './generateCliReport';
|
||||
import { logApiReportInstructions } from '../common';
|
||||
|
||||
function parseHelpPage(helpPageContent: string) {
|
||||
const [, usage] = helpPageContent.match(/^\s*Usage: (.*)$/im) ?? [];
|
||||
let usage: string | undefined;
|
||||
|
||||
// Commander format: "Usage: backstage-cli ..."
|
||||
const commanderUsage = helpPageContent.match(/^\s*Usage: (.*)$/im);
|
||||
if (commanderUsage) {
|
||||
usage = commanderUsage[1];
|
||||
}
|
||||
|
||||
const lines = helpPageContent.split(/\r?\n/);
|
||||
|
||||
let options = new Array<string>();
|
||||
@@ -39,8 +46,8 @@ function parseHelpPage(helpPageContent: string) {
|
||||
lines.shift();
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
// Start of a new section, e.g. "Options:"
|
||||
const sectionName = lines.shift();
|
||||
// Start of a new section, e.g. "Options:" or "FLAGS:"
|
||||
const sectionName = lines.shift()?.toLocaleLowerCase('en-US');
|
||||
// Take lines until we hit the next section or the end
|
||||
const sectionEndIndex = lines.findIndex(
|
||||
line => line && !line.match(/^\s/),
|
||||
@@ -53,12 +60,18 @@ function parseHelpPage(helpPageContent: string) {
|
||||
.map(line => line.match(/^\s{1,8}(.*?)\s\s+/)?.[1])
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (sectionName?.toLocaleLowerCase('en-US') === 'options:') {
|
||||
if (sectionName === 'options:' || sectionName === 'flags:') {
|
||||
options = sectionItems;
|
||||
} else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') {
|
||||
} else if (sectionName === 'commands:') {
|
||||
commands = sectionItems;
|
||||
} else if (sectionName?.toLocaleLowerCase('en-US') === 'arguments:') {
|
||||
} else if (sectionName === 'arguments:') {
|
||||
commandArguments = sectionItems;
|
||||
} else if (sectionName === 'usage:') {
|
||||
// cleye format: usage line is inside the USAGE: section
|
||||
const usageLine = sectionLines.find(l => l.trim().length > 0)?.trim();
|
||||
if (usageLine) {
|
||||
usage = usageLine;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown CLI section: ${sectionName}`);
|
||||
}
|
||||
|
||||
@@ -14,58 +14,63 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import pLimit from 'p-limit';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { openSync, closeSync, readFileSync, unlinkSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
// Some commands launch full node processes doing heavy work, which at high
|
||||
// concurrency levels risk exhausting system resources. Placing the limiter here
|
||||
// at the root level ensures that the concurrency boundary applies globally, not
|
||||
// just per-runner.
|
||||
const limiter = pLimit(os.cpus().length);
|
||||
// Matches ANSI SGR escape sequences (e.g. bold, color, reset)
|
||||
const ansiPattern = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;]*m`, 'g');
|
||||
|
||||
/**
|
||||
* Redirect stdout to a temp file so that Node.js creates a SyncWriteStream
|
||||
* (synchronous writes) in the child instead of an async pipe stream. This
|
||||
* prevents data loss when child processes call process.exit() before the
|
||||
* async stream buffer has been flushed.
|
||||
*
|
||||
* Uses spawnSync which blocks the event loop, so no concurrency limiter is
|
||||
* needed — each call naturally runs sequentially.
|
||||
*/
|
||||
export function createBinRunner(cwd: string, path: string) {
|
||||
return async (...command: string[]) =>
|
||||
limiter(
|
||||
() =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
// Handle the case where path is empty and the script path is the first command argument
|
||||
const args = path ? [path, ...command] : command;
|
||||
const child = spawn('node', args, {
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
return async (...command: string[]) => {
|
||||
const args = path ? [path, ...command] : command;
|
||||
const outPath = join(tmpdir(), `backstage-cli-out-${randomUUID()}.txt`);
|
||||
const outFd = openSync(outPath, 'w');
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
try {
|
||||
const result = spawnSync('node', args, {
|
||||
cwd,
|
||||
env: { ...process.env, NO_COLOR: '1' },
|
||||
stdio: ['ignore', outFd, 'pipe'],
|
||||
});
|
||||
|
||||
child.stdout?.on('data', data => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
closeSync(outFd);
|
||||
const stdout = readFileSync(outPath, 'utf8').replace(ansiPattern, '');
|
||||
|
||||
child.stderr?.on('data', data => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
if (result.error) {
|
||||
throw new Error(`Process error: ${result.error.message}`);
|
||||
}
|
||||
|
||||
child.on('error', err => {
|
||||
reject(new Error(`Process error: ${err.message}`));
|
||||
});
|
||||
const stderr = result.stderr?.toString() ?? '';
|
||||
|
||||
child.on('close', (code, signal) => {
|
||||
if (signal) {
|
||||
reject(
|
||||
new Error(
|
||||
`Process was killed with signal ${signal}\n${stderr}`,
|
||||
),
|
||||
);
|
||||
} else if (code !== 0) {
|
||||
reject(new Error(`Process exited with code ${code}\n${stderr}`));
|
||||
} else if (stderr.trim()) {
|
||||
reject(new Error(`Command printed error output: ${stderr}`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (result.signal) {
|
||||
throw new Error(
|
||||
`Process was killed with signal ${result.signal}\n${stderr}`,
|
||||
);
|
||||
} else if (result.status !== 0) {
|
||||
throw new Error(`Process exited with code ${result.status}\n${stderr}`);
|
||||
} else if (stderr.trim()) {
|
||||
throw new Error(`Command printed error output: ${stderr}`);
|
||||
}
|
||||
|
||||
return stdout;
|
||||
} finally {
|
||||
try {
|
||||
unlinkSync(outPath);
|
||||
} catch {
|
||||
/* ignore cleanup errors */
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3352,6 +3352,7 @@ __metadata:
|
||||
buffer: "npm:^6.0.3"
|
||||
chalk: "npm:^4.0.0"
|
||||
chokidar: "npm:^3.3.1"
|
||||
cleye: "npm:^2.2.1"
|
||||
commander: "npm:^14.0.3"
|
||||
cross-fetch: "npm:^4.0.0"
|
||||
cross-spawn: "npm:^7.0.3"
|
||||
@@ -27246,6 +27247,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cleye@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "cleye@npm:2.2.1"
|
||||
dependencies:
|
||||
terminal-columns: "npm:^2.0.0"
|
||||
type-flag: "npm:^4.0.3"
|
||||
checksum: 10/4d24a7e4863d38b9a40c01ca332baa7526b320c14fc488e7fb2d3f54b0ad5baa8c5defa6fc6adc079f50f629409056d9da74be6611f6394b709c9062d39b2216
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-boxes@npm:^2.2.0, cli-boxes@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "cli-boxes@npm:2.2.1"
|
||||
@@ -48464,6 +48475,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"terminal-columns@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "terminal-columns@npm:2.0.0"
|
||||
checksum: 10/f958993886e09d7c0780f47833316532a0c7dd7db2fb43da9d34a88c821633408a6e7084af59f99c20b045776814748f14a8e33573886186be7a30174092964f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"terser-webpack-plugin@npm:*, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.16":
|
||||
version: 5.3.16
|
||||
resolution: "terser-webpack-plugin@npm:5.3.16"
|
||||
@@ -49323,6 +49341,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-flag@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "type-flag@npm:4.0.3"
|
||||
checksum: 10/ffa9e56e50ed73ed802aa45b8d47eeba7677d2d66785e9153bfbc758c2613b6ba78b621d7766b713a287dbbe6758a61000f57d2ce8afc94e1876a37de914c73c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-is@npm:^1.6.18, type-is@npm:~1.6.18":
|
||||
version: 1.6.18
|
||||
resolution: "type-is@npm:1.6.18"
|
||||
|
||||
Reference in New Issue
Block a user