Skip to content

Commit ef2d62b

Browse files
committed
chore: hidden help command
1 parent fefdf64 commit ef2d62b

File tree

14 files changed

+385
-29
lines changed

14 files changed

+385
-29
lines changed

eslint.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,10 @@ export default [
8686
],
8787
},
8888
},
89+
{
90+
files: ['features/**/*'],
91+
rules: {
92+
'import/no-extraneous-dependencies': 'off',
93+
},
94+
},
8995
];

features/help-command.feature.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Feature: Help command (`apify help`)
2+
3+
- As a CLI user
4+
- I want to get easy help messages
5+
6+
## Rule: `apify help` prints the whole help message
7+
8+
### Example: call `apify help` with no commands
9+
10+
- When I run anywhere:
11+
```
12+
$ apify help
13+
```
14+
- Then I can read text on stdout:
15+
```
16+
https://apify.com/contact
17+
```
18+
19+
### Example: call `actor help` with no commands
20+
21+
- When I run anywhere:
22+
```
23+
$ actor help
24+
```
25+
- Then I can read text on stdout:
26+
```
27+
https://apify.com/contact
28+
```
29+
30+
### Example: call `apify help help` prints the main message
31+
32+
- When I run anywhere:
33+
```
34+
$ apify help help
35+
```
36+
- Then I can read text on stdout:
37+
```
38+
https://apify.com/contact
39+
```
40+
41+
## Rule: `apify help <command>` prints a command's help message
42+
43+
### Example: `apify help -h`
44+
45+
- When I run anywhere:
46+
```
47+
$ apify help -h
48+
```
49+
- Then I can read text on stdout:
50+
```
51+
Prints out help about a command, or all available commands.
52+
```
53+
54+
### Example: `apify help run`
55+
56+
- When I run anywhere:
57+
```
58+
$ apify help run
59+
```
60+
- Then I can read text on stdout:
61+
```
62+
Executes Actor locally with simulated Apify environment variables.
63+
```
64+
65+
### Example: `apify run --help` returns the same message as `apify help run`
66+
67+
- When I run anywhere:
68+
```
69+
$ apify run --help
70+
```
71+
- Then I can read text on stdout:
72+
```
73+
Executes Actor locally with simulated Apify environment variables.
74+
```

features/test-implementations/0.world.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
55
import type { IWorld } from '@cucumber/cucumber';
66
import { Result } from '@sapphire/result';
77
import type { ApifyClient } from 'apify-client';
8-
import { type Options, type ExecaError, type Result as ExecaResult, execaNode } from 'execa';
8+
import { type ExecaError, execaNode, type Options, type Result as ExecaResult } from 'execa';
99

1010
type DynamicOptions = {
1111
-readonly [P in keyof Options]: Options[P];
@@ -49,8 +49,8 @@ export interface TestWorld<Parameters = unknown[]> extends IWorld<Parameters> {
4949
*/
5050
export const ProjectRoot = new URL('../../', import.meta.url);
5151

52-
export const DevRunFile = new URL('./src/entrypoints/apify.ts', ProjectRoot);
53-
52+
export const ApifyDevRunFile = new URL('./src/entrypoints/apify.ts', ProjectRoot);
53+
export const ActorDevRunFile = new URL('./src/entrypoints/actor.ts', ProjectRoot);
5454
export const TestTmpRoot = new URL('./test/tmp/', ProjectRoot);
5555

5656
await mkdir(TestTmpRoot, { recursive: true });
@@ -80,18 +80,20 @@ export async function executeCommand({
8080
// step 1: get the first element, and make sure it starts with `apify`
8181
const [command] = commandToRun;
8282

83-
if (!command.startsWith('apify')) {
83+
if (!command.startsWith('apify') && !command.startsWith('actor')) {
8484
// TODO: maybe try to parse these commands out and provide stdin that way, but for now, its better to get the writer to use the existing rules
8585
if (command.startsWith('echo') || command.startsWith('jo')) {
8686
throw new RangeError(
8787
`When writing a test case, please use the "given the following input provided via standard input" rule for providing standard input to the command you're testing.\nReceived: ${command}`,
8888
);
8989
}
9090

91-
throw new RangeError(`Command must start with 'apify', received: ${command}`);
91+
throw new RangeError(`Command must start with 'apify' or 'actor', received: ${command}`);
9292
}
9393

94-
const cleanCommand = command.replace(/^apify/, '').trim();
94+
const devRunFile = command.startsWith('apify') ? ApifyDevRunFile : ActorDevRunFile;
95+
96+
const cleanCommand = command.replace(/^apify|actor/, '').trim();
9597

9698
const options: DynamicOptions = {
9799
cwd,
@@ -138,7 +140,7 @@ export async function executeCommand({
138140
>(async () => {
139141
const process = execaNode(
140142
tsxCli,
141-
[fileURLToPath(DevRunFile), ...commandArguments],
143+
[fileURLToPath(devRunFile), ...commandArguments],
142144
options as { cwd: typeof cwd; input: typeof stdin },
143145
);
144146

features/test-implementations/1.setup.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { readFile, rm, writeFile } from 'node:fs/promises';
44
import { AfterAll, Given, setDefaultTimeout } from '@cucumber/cucumber';
55
import { ApifyClient } from 'apify-client';
66

7+
import { getApifyClientOptions } from '../../src/lib/utils.js';
78
import {
89
assertWorldIsLoggedIn,
910
assertWorldIsValid,
@@ -12,7 +13,6 @@ import {
1213
TestTmpRoot,
1314
type TestWorld,
1415
} from './0.world';
15-
import { getApifyClientOptions } from '../../src/lib/utils';
1616

1717
setDefaultTimeout(20_000);
1818

@@ -230,7 +230,6 @@ Given<TestWorld>(/the local actor is pushed to the Apify platform/i, { timeout:
230230
const extraEnv: Record<string, string> = {};
231231

232232
if (this.authStatePath) {
233-
// eslint-disable-next-line no-underscore-dangle
234233
extraEnv.__APIFY_INTERNAL_TEST_AUTH_PATH__ = this.authStatePath;
235234
}
236235

features/test-implementations/2.execution.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,51 @@ import {
1010
type TestWorld,
1111
} from './0.world';
1212

13+
When<TestWorld>(/i run anywhere:?$/i, async function (commandBlock: string) {
14+
if (typeof commandBlock !== 'string') {
15+
throw new TypeError('When using the `I run anywhere` step, you must provide a text block containing a command');
16+
}
17+
18+
const extraEnv: Record<string, string> = {};
19+
20+
if (this.authStatePath) {
21+
extraEnv.__APIFY_INTERNAL_TEST_AUTH_PATH__ = this.authStatePath;
22+
}
23+
24+
const result = await executeCommand({
25+
rawCommand: commandBlock,
26+
env: extraEnv,
27+
});
28+
29+
if (result.isOk()) {
30+
const value = result.unwrap();
31+
32+
if (this.testResults) {
33+
console.error(`\n Warning: Overwriting existing test results: ${JSON.stringify(this.testResults)}`);
34+
}
35+
36+
this.testResults = {
37+
exitCode: value.exitCode!,
38+
stderr: value.stderr,
39+
stdout: value.stdout,
40+
runResults: null,
41+
};
42+
} else {
43+
const error = result.unwrapErr();
44+
45+
if (this.testResults) {
46+
console.error(`\n Warning: Overwriting existing test results: ${JSON.stringify(this.testResults)}`);
47+
}
48+
49+
this.testResults = {
50+
exitCode: error.exitCode!,
51+
stderr: error.stderr,
52+
stdout: error.stdout,
53+
runResults: null,
54+
};
55+
}
56+
});
57+
1358
When<TestWorld>(/i run:?$/i, async function (commandBlock: string) {
1459
assertWorldIsValid(this);
1560

@@ -20,7 +65,6 @@ When<TestWorld>(/i run:?$/i, async function (commandBlock: string) {
2065
const extraEnv: Record<string, string> = {};
2166

2267
if (this.authStatePath) {
23-
// eslint-disable-next-line no-underscore-dangle
2468
extraEnv.__APIFY_INTERNAL_TEST_AUTH_PATH__ = this.authStatePath;
2569
}
2670

@@ -103,7 +147,6 @@ When<TestWorld>(/i run with captured data/i, async function (commandBlock: strin
103147
const extraEnv: Record<string, string> = {};
104148

105149
if (this.authStatePath) {
106-
// eslint-disable-next-line no-underscore-dangle
107150
extraEnv.__APIFY_INTERNAL_TEST_AUTH_PATH__ = this.authStatePath;
108151
}
109152

features/test-implementations/3.results.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ Then<TestWorld>(/i don't see any node\.js exception/i, function () {
7777
});
7878

7979
Then<TestWorld>(/i can read text on stderr/i, function (expectedStdout: string) {
80-
assertWorldIsValid(this);
8180
assertWorldHasRanCommand(this);
8281

8382
if (typeof expectedStdout !== 'string') {
@@ -87,9 +86,16 @@ Then<TestWorld>(/i can read text on stderr/i, function (expectedStdout: string)
8786
}
8887

8988
const lowercasedResult = this.testResults.stderr.toLowerCase();
90-
const lowercasedExpected = replaceMatchersInString(expectedStdout, {
91-
testActorName: this.testActor.name,
92-
}).toLowerCase();
89+
90+
let lowercasedExpected = expectedStdout;
91+
92+
if (this.testActor) {
93+
lowercasedExpected = replaceMatchersInString(lowercasedExpected, {
94+
testActorName: this.testActor.name,
95+
});
96+
}
97+
98+
lowercasedExpected = lowercasedExpected.toLowerCase();
9399

94100
strictEqual(
95101
lowercasedResult.includes(lowercasedExpected),
@@ -99,7 +105,6 @@ Then<TestWorld>(/i can read text on stderr/i, function (expectedStdout: string)
99105
});
100106

101107
Then<TestWorld>(/i can read text on stdout/i, function (expectedStdout: string) {
102-
assertWorldIsValid(this);
103108
assertWorldHasRanCommand(this);
104109

105110
if (typeof expectedStdout !== 'string') {
@@ -109,9 +114,16 @@ Then<TestWorld>(/i can read text on stdout/i, function (expectedStdout: string)
109114
}
110115

111116
const lowercasedResult = this.testResults.stdout.toLowerCase();
112-
const lowercasedExpected = replaceMatchersInString(expectedStdout, {
113-
testActorName: this.testActor.name,
114-
}).toLowerCase();
117+
118+
let lowercasedExpected = expectedStdout;
119+
120+
if (this.testActor) {
121+
lowercasedExpected = replaceMatchersInString(lowercasedExpected, {
122+
testActorName: this.testActor.name,
123+
});
124+
}
125+
126+
lowercasedExpected = lowercasedExpected.toLowerCase();
115127

116128
strictEqual(
117129
lowercasedResult.includes(lowercasedExpected),

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
"types": "./dist/index.d.ts",
77
"type": "module",
88
"scripts": {
9-
"dev": "tsx ./src/entrypoints/apify.ts",
9+
"dev:apify": "tsx ./src/entrypoints/apify.ts",
10+
"dev:actor": "tsx ./src/entrypoints/actor.ts",
1011
"test": "vitest run",
1112
"test-python": "vitest run -t '.*\\[python\\]'",
1213
"test:cucumber": "cross-env NODE_OPTIONS=\"--import tsx\" cucumber-js",
13-
"lint": "eslint src test scripts --ext .ts,.cjs,.mjs",
14-
"lint:fix": "eslint src test scripts --fix --ext .ts,.cjs,.mjs",
14+
"lint": "eslint src test scripts features --ext .ts,.cjs,.mjs",
15+
"lint:fix": "eslint src test scripts features --fix --ext .ts,.cjs,.mjs",
1516
"format": "biome format . && prettier --check \"**/*.{md,yml,yaml}\"",
1617
"format:fix": "biome format --write . && prettier --write \"**/*.{md,yml,yaml}\"",
1718
"clean": "rimraf dist",
@@ -69,6 +70,7 @@
6970
"@sapphire/duration": "^1.1.2",
7071
"@sapphire/result": "^2.7.2",
7172
"@sapphire/timestamp": "^1.0.3",
73+
"@skyra/jaro-winkler": "^1.1.1",
7274
"adm-zip": "~0.5.15",
7375
"ajv": "~8.17.1",
7476
"apify-client": "^2.11.0",

src/commands/_register.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import type { BuiltApifyCommand } from '../lib/command-framework/apify-command.js';
22
import { ActorIndexCommand } from './actor/_index.js';
3+
import { ActorChargeCommand } from './actor/charge.js';
4+
import { ActorGetInputCommand } from './actor/get-input.js';
5+
import { ActorGetPublicUrlCommand } from './actor/get-public-url.js';
6+
import { ActorGetValueCommand } from './actor/get-value.js';
7+
import { ActorPushDataCommand } from './actor/push-data.js';
8+
import { ActorSetValueCommand } from './actor/set-value.js';
39
import { ActorsIndexCommand } from './actors/_index.js';
410
import { BuildsIndexCommand } from './builds/_index.js';
511
import { TopLevelCallCommand } from './call.js';
612
import { CheckVersionCommand } from './check-version.js';
713
import { CreateCommand } from './create.js';
814
import { DatasetsIndexCommand } from './datasets/_index.js';
915
import { EditInputSchemaCommand } from './edit-input-schema.js';
16+
import { HelpCommand } from './help.js';
1017
import { InfoCommand } from './info.js';
1118
import { InitCommand } from './init.js';
1219
import { WrapScrapyCommand } from './init-wrap-scrapy.js';
@@ -47,8 +54,16 @@ export const apifyCommands = [
4754
ToplevelPushCommand,
4855
RunCommand,
4956
ValidateInputSchemaCommand,
57+
HelpCommand,
5058
] as const satisfies (typeof BuiltApifyCommand)[];
5159

52-
export const actorCommands: (typeof BuiltApifyCommand)[] = [
60+
export const actorCommands = [
5361
//
54-
];
62+
ActorSetValueCommand,
63+
ActorPushDataCommand,
64+
ActorGetValueCommand,
65+
ActorGetPublicUrlCommand,
66+
ActorGetInputCommand,
67+
ActorChargeCommand,
68+
HelpCommand,
69+
] as const satisfies (typeof BuiltApifyCommand)[];

0 commit comments

Comments
 (0)