Skip to content

Commit a05bfdc

Browse files
committed
feat(pk-cli): setup workspace project, implement command fetch
1 parent 662d115 commit a05bfdc

File tree

10 files changed

+268
-0
lines changed

10 files changed

+268
-0
lines changed

packages/pk-cli/.eslintrc.cjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const baseEslintConfig = require('@pkerschbaum/config-eslint/eslint-ecma.cjs');
2+
3+
module.exports = {
4+
...baseEslintConfig,
5+
parserOptions: {
6+
...baseEslintConfig.parserOptions,
7+
tsconfigRootDir: __dirname,
8+
},
9+
ignorePatterns: [...(baseEslintConfig.ignorePatterns || []), '**/bin/pk-cli.js'],
10+
rules: {
11+
...baseEslintConfig.rules,
12+
/* allow for this package to use console logs - is a CLI application */
13+
'no-console': 'off',
14+
},
15+
};

packages/pk-cli/bin/pk-cli.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env node
2+
3+
try {
4+
await import('../dist/cli.js');
5+
} catch (err) {
6+
if (
7+
err.code === 'ERR_MODULE_NOT_FOUND' &&
8+
err.message.includes('Cannot find package') &&
9+
err.message.includes('dist/cli.js')
10+
) {
11+
throw new Error(
12+
`Could not find JS code to execute! Build this workspace project to fix this.`,
13+
{ cause: err },
14+
);
15+
}
16+
throw err;
17+
}

packages/pk-cli/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@pkerschbaum/pk-cli",
3+
"private": true,
4+
"type": "module",
5+
"exports": {
6+
".": null,
7+
"./*": null
8+
},
9+
"bin": {
10+
"pk": "./bin/pk-cli.js"
11+
},
12+
"files": [
13+
"dist/**",
14+
"!dist/**/*.d.ts.map"
15+
],
16+
"scripts": {
17+
"build": "pnpm run internal:compile",
18+
"dev": "pnpm run build --watch --preserveWatchOutput",
19+
"internal:compile": "tsc -p ./tsconfig.project.json",
20+
"lint": "pnpm run lint:file .",
21+
"lint:file": "eslint --max-warnings 0",
22+
"lint:fix": "pnpm run lint --fix",
23+
"nuke": "pnpm run nuke:artifacts && del-cli node_modules",
24+
"nuke:artifacts": "del-cli dist \"*.tsbuildinfo\""
25+
},
26+
"dependencies": {
27+
"@commander-js/extra-typings": "^14.0.0",
28+
"@pkerschbaum/commons-ecma": "workspace:*",
29+
"@pkerschbaum/commons-node": "workspace:*",
30+
"@pkerschbaum/runtime-extensions-node": "workspace:*",
31+
"commander": "^14.0.0",
32+
"tiny-invariant": "^1.3.3"
33+
},
34+
"devDependencies": {
35+
"@types/node": "^20"
36+
}
37+
}

packages/pk-cli/src/cli.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import '@pkerschbaum/runtime-extensions-node';
2+
3+
import * as commander from '@commander-js/extra-typings';
4+
import fs from 'node:fs';
5+
import stream from 'node:stream';
6+
7+
import { fetchCommand } from '#pkg/commands/fetch.js';
8+
import type { CommandResult } from '#pkg/types.js';
9+
10+
const outputTypes = ['console', 'file'] as const;
11+
type OutputType = (typeof outputTypes)[number];
12+
13+
const runCommand = new commander.Command('fetch')
14+
.addOption(
15+
new commander.Option('--output <output-type>')
16+
.choices(outputTypes)
17+
.makeOptionMandatory()
18+
.default('console'),
19+
)
20+
.addOption(new commander.Option('--include-body'))
21+
.addArgument(new commander.Argument('<url>'))
22+
.action(async (url, options) => {
23+
const commandResult = await fetchCommand({ url, includeBody: options.includeBody });
24+
await outputTypeToPrintFn[options.output](commandResult);
25+
});
26+
27+
const program = new commander.Command().addCommand(runCommand);
28+
program.parse();
29+
30+
const outputTypeToPrintFn: Record<
31+
OutputType,
32+
(commandResults: CommandResult[]) => void | Promise<void>
33+
> = {
34+
console: async (commandResults) => {
35+
for (const result of commandResults) {
36+
console.log(`\n--- Output for ${result.name} ---`);
37+
try {
38+
for await (const chunk of result.output) {
39+
process.stdout.write(chunk);
40+
}
41+
} catch (error) {
42+
console.error(`Error reading stream for ${result.name}:`, error);
43+
}
44+
process.stdout.write('\n');
45+
}
46+
},
47+
file: async (commandResults) => {
48+
const timestamp = new Date().toISOString().replace(/[-:.]/g, '');
49+
for (const [index, result] of commandResults.entries()) {
50+
const filePath = `./${timestamp}-${index + 1}-${result.name}.txt`;
51+
const fileStream = fs.createWriteStream(filePath, { encoding: 'utf8' });
52+
try {
53+
await stream.promises.pipeline(result.output, fileStream);
54+
} catch (error) {
55+
console.error(`Error writing file ${filePath}:`, error);
56+
// Clean up partially written file on error
57+
try {
58+
await fs.promises.unlink(filePath);
59+
} catch {
60+
// ignore
61+
}
62+
}
63+
}
64+
},
65+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ReadableStream } from 'node:stream/web';
2+
3+
import type { Command, CommandResult } from '#pkg/types.js';
4+
5+
export { fetchCommand };
6+
7+
const fetchCommand = (async (opts: { url: string; includeBody?: boolean }) => {
8+
const url = new URL(opts.url);
9+
const response = await fetch(url, { redirect: 'manual' });
10+
11+
let metaOutput = '';
12+
metaOutput += `Status: ${response.status}\n`;
13+
metaOutput += `Headers: \n`;
14+
for (const [name, value] of response.headers) {
15+
metaOutput += ` ${name}: ${value}\n`;
16+
}
17+
18+
const metaStream = ReadableStream.from([metaOutput]);
19+
const bodyStream = response.body ?? ReadableStream.from([]);
20+
21+
const result: CommandResult[] = [{ name: 'meta', output: metaStream }];
22+
if (opts.includeBody) {
23+
result.push({ name: 'body', output: bodyStream });
24+
}
25+
return result;
26+
}) satisfies Command;

packages/pk-cli/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { ReadableStream } from 'node:stream/web';
2+
3+
export type Command = (
4+
opts: // eslint-disable-next-line @typescript-eslint/no-explicit-any -- this type is used with `satisfies`, we don't mind about this any here
5+
any,
6+
) => Promise<CommandResult[]>;
7+
export type CommandResult = {
8+
name: string;
9+
output: ReadableStream<ChunkType> | globalThis.ReadableStream<ChunkType>;
10+
};
11+
12+
type ChunkType = string | Uint8Array;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"extends": "@pkerschbaum/config-typescript/tsconfig.json",
3+
"compilerOptions": {
4+
/* Modules */
5+
"paths": {
6+
"#pkg/*": ["./src/*"]
7+
},
8+
"rootDir": "./src",
9+
"types": ["node"],
10+
11+
/* Emit */
12+
"outDir": "./dist"
13+
},
14+
"include": ["src/**/*"]
15+
}

packages/pk-cli/turbo.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"$schema": "https://turbo.build/schema.json",
3+
"extends": ["//"],
4+
"tasks": {
5+
"build": {
6+
"cache": true,
7+
"inputs": [
8+
"../../platform/config-typescript/tsconfig.json",
9+
"src/**",
10+
"package.json",
11+
"tsconfig.project.json"
12+
],
13+
"outputs": ["dist/**", "*.tsbuildinfo"]
14+
},
15+
"lint": {
16+
"cache": true,
17+
"inputs": [
18+
"../../platform/config-typescript/tsconfig.json",
19+
"../../platform/config-eslint/eslint-ecma.cjs",
20+
"src/**",
21+
".eslintrc.cjs",
22+
"package.json",
23+
"tsconfig.project.json"
24+
],
25+
"outputs": []
26+
},
27+
"lint:fix": {
28+
"cache": true,
29+
"inputs": [
30+
"../../platform/config-typescript/tsconfig.json",
31+
"../../platform/config-eslint/eslint-ecma.cjs",
32+
"src/**",
33+
".eslintrc.cjs",
34+
"package.json",
35+
"tsconfig.project.json"
36+
],
37+
"outputs": ["src/**"]
38+
}
39+
}
40+
}

pnpm-lock.yaml

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
{ "path": "./packages/eslint-plugin-code-import-patterns/tsconfig.project.json" },
77
{ "path": "./packages/fetch-favicon/tsconfig.project.json" },
88
{ "path": "./packages/fetch-sitemap-locations/tsconfig.project.json" },
9+
{ "path": "./packages/pk-cli/tsconfig.project.json" },
910
{ "path": "./packages/pk-web-stack-codemods/tsconfig.project.json" },
1011
{ "path": "./packages/pkg-consumption-test/tsconfig.project.json" },
1112
{ "path": "./packages/pkg-management/tsconfig.project.json" },

0 commit comments

Comments
 (0)