Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Jan 18, 2025

Summary by CodeRabbit

Here are the release notes for the pull request:

  • New Features

    • Added CLI commands for managing API keys
    • Introduced report generation functionality for API status
    • Enhanced environment switching capabilities
    • Added token validation command
  • Improvements

    • Refactored CLI infrastructure using NestJS Commander
    • Improved logging and error handling
    • Updated configuration file management
  • Changes

    • Removed several legacy CLI command implementations
    • Updated dependency management
    • Modified configuration type handling
  • Documentation

    • Updated README with new API key and reporting commands

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This pull request introduces a comprehensive refactoring of the Unraid API's command-line interface (CLI), transitioning from a custom implementation to the NestJS Commander library. The changes involve removing multiple existing command files and replacing them with a new, more structured approach to CLI commands. Key additions include new commands for key management, reporting, environment switching, and other API-related operations. The project also updates configuration handling, introduces new logging services, and modifies the overall CLI architecture to provide more robust and flexible command-line interactions.

Changes

File Change Summary
api/README.md Added sections for API key management, reporting, secrets, and licensing
api/docker-compose.yml Removed version declaration version: '3.8'
api/package.json Added nest-commander dependency, removed ts-command-line-args, added new command script
api/src/cli* Completely restructured CLI implementation using NestJS Commander
api/src/types/my-servers-config.ts Introduced Zod schemas for configuration validation
api/src/unraid-api/cli/* Added new command classes for key management, reporting, start/stop, etc.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant APIKeyService
    participant ReportGenerator
    
    User->>CLI: Run command (e.g., key create)
    CLI->>APIKeyService: Validate and create key
    APIKeyService-->>CLI: Return key details
    CLI->>User: Display key information

    User->>CLI: Run report command
    CLI->>ReportGenerator: Generate system report
    ReportGenerator-->>CLI: Collect system data
    CLI->>User: Display report details
Loading

Possibly related PRs

Suggested reviewers

  • mdatelle

Poem

🐰 A Rabbit's CLI Delight

With NestJS Commander's might,
Commands dance with grace so bright,
Keys and reports now take flight,
Our API sings with pure delight!

Hop, hop, hooray! 🚀


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@elibosley elibosley changed the base branch from main to feat/add-cli-command-for-api-keys January 18, 2025 15:43
@elibosley elibosley changed the title feat: create key cli command logic and add to index command list feat use zod for config parsing Jan 18, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 13

🧹 Nitpick comments (24)
api/src/core/utils/files/config-file-normalizer.ts (1)

43-43: Avoid using as any to maintain type safety

Casting the result to any in return schema.parse(mergedConfig) as any undermines TypeScript's type safety. Consider refining the types or leveraging generic type parameters to ensure strong typing without resorting to any.

You might adjust the return statement to maintain type safety:

-return schema.parse(mergedConfig) as any; // Narrowing ensures correct typing
+return schema.parse(mergedConfig) as T extends 'memory' ? MyServersConfigMemory : MyServersConfig;
api/src/unraid-api/cli/validate-token.command.ts (1)

76-77: Nitpick: Improve error message clarity

Consider rephrasing the error message for better clarity and readability:

     this.logger.error(
-        createJsonErrorString('Username on token does not match logged in user name')
+        createJsonErrorString('Username in token does not match logged-in user name')
     );
api/src/unraid-api/cli/switch-env.command.ts (2)

83-86: Ensure consistent usage of the logger instance.

In lines 83 and 86, you are using cliLogger instead of this.logger, whereas the rest of the class uses this.logger for logging. For consistency and to adhere to best practices, consider using this.logger throughout the class.

Apply this diff to make the change:

-        cliLogger.debug('Copying %s to %s', source, destination);
+        this.logger.debug('Copying %s to %s', source, destination);

...

-        cliLogger.info('Now using %s', newEnv);
+        this.logger.info('Now using %s', newEnv);

77-85: Add error handling for filesystem operations.

The writeFile and copyFile operations don't have error handling. If these operations fail, it could lead to unexpected behavior without clear feedback. Consider wrapping them in try-catch blocks to handle potential errors appropriately.

Example implementation:

try {
  await writeFile(envFlashFilePath, newEnvLine);
} catch (error) {
  this.logger.error('Failed to write to %s: %s', envFlashFilePath, error.message);
  throw error;
}

try {
  await copyFile(source, destination);
} catch (error) {
  this.logger.error('Failed to copy file from %s to %s: %s', source, destination, error.message);
  throw error;
}
api/src/unraid-api/cli/key.command.ts (3)

69-69: Replace console.log with the logger for consistent logging.

Using console.log in production code is discouraged. Replace it with this.logger.log to maintain consistent logging practices.

Apply this diff:

-        console.log(options, passedParams);
+        this.logger.debug('Options: %o, Params: %o', options, passedParams);

84-88: Adjust logic considering unimplemented permissions feature.

Since permissions are not yet implemented, checking for options.permissions may cause confusion. Adjust the error message and logic to reflect the current state.

Apply this diff to focus on roles:

-        if (options.roles?.length === 0 && options.permissions?.length === 0) {
+        if (!options.roles || options.roles.length === 0) {
             this.logger.error(
-                'Please add at least one role or permission with --roles or --permissions'
+                'Please specify at least one role with --roles'
             );
             return;
         }

64-66: Handle unimplemented method without throwing an error.

The parsePermissions method throws an error indicating it's a stub. Throwing errors in option parsers can lead to unexpected behavior in the CLI. Instead, consider logging a warning or info message and returning a default value.

Apply this diff to prevent the method from throwing an error:

-        throw new Error('Stub Method Until Permissions PR is merged');
+        this.logger.warn('Permissions feature is not yet implemented.');
+        return [];
api/src/unraid-api/cli/report.command.ts (3)

237-237: Replace void with undefined in the return type for clarity.

Using void in a union type can be confusing and may not behave as expected. Replace void with undefined to make the return type explicit.

Apply this diff:

-        async report(options?: ReportOptions): Promise<string | ReportObject | void> {
+        async report(options?: ReportOptions): Promise<string | ReportObject | undefined> {
🧰 Tools
🪛 Biome (1.9.4)

[error] 237-237: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)


364-370: Ensure consistent error handling without using console.log.

Using console.log for error handling is inconsistent with the rest of the code, which uses the logger. Replace console.log with this.logger.error or this.logger.debug as appropriate.

Apply this diff:

-            console.log({ error });
+            this.logger.debug('Error generating report:', error);
             if (error instanceof Error) {
-                this.logger.debug(error);
+                this.logger.debug(error.stack);
                 this.logger.error(`\nFailed generating report with "${error.message}"\n`);
                 return;
             }

238-248: Simplify TTY check and adjust logic for raw output.

The logic for determining isInteractive can be simplified. Ensure that the rawOutput flag correctly influences the output format.

Consider revising this block:

const isInteractive = process.stdout.isTTY && !rawOutput;

Ensure that all cases where rawOutput affects the output are consistently handled throughout the method.

api/src/unraid-api/cli/version.command.ts (2)

6-7: Add command description for better CLI help output.

The command decorator is missing a description that would help users understand its purpose.

-@Command({ name: 'version' })
+@Command({ name: 'version', description: 'Display the current API version' })

11-13: Add error handling for undefined API_VERSION.

The implementation should handle cases where API_VERSION might be undefined.

     async run(): Promise<void> {
+        if (!API_VERSION) {
+            this.logger.error('API version is not defined');
+            return;
+        }
         this.logger.info(`Unraid API v${API_VERSION}`);
     }
api/src/unraid-api/cli/report.spec.ts (1)

7-10: Consider using beforeEach for better test isolation.

Using beforeAll for store configuration might cause test interdependence. Consider using beforeEach for better isolation.

-beforeAll(async () => {
+beforeEach(async () => {
     // Load cfg into store
     await store.dispatch(loadConfigFile());
 });
api/src/unraid-api/cli/stop.command.ts (1)

7-9: Add command description for better CLI help output.

The command decorator is missing a description that would help users understand its purpose.

 @Command({
     name: 'stop',
+    description: 'Stop the unraid-api service',
 })
api/src/unraid-api/cli/log.service.ts (2)

27-29: Consider adding type safety to debug parameters

The debug method accepts any type parameters which reduces type safety. Consider using a more specific type or generic constraint.

-    debug(message: any, ...optionalParams: any[]): void {
+    debug<T>(message: T, ...optionalParams: unknown[]): void {
         this.logger.debug(message, ...optionalParams);
     }

5-29: Consider enhancing the logging implementation

The current implementation is a simple wrapper around console methods. Consider these improvements:

  1. Add timestamp prefixes to log messages
  2. Add color support for different log levels using a library like chalk
  3. Add log level configuration
  4. Consider using Winston or Pino instead of console for production logging

Would you like me to provide an enhanced implementation with these features?

api/src/unraid-api/cli/restart.command.ts (1)

1-16: Clean up imports and add missing dependencies

Remove empty lines between imports and add missing dependencies.

 import { execSync } from 'child_process';
 import { join } from 'path';
-
-
-
 import { Command, CommandRunner } from 'nest-commander';
-
-
-
 import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts';
+import { LogService } from '@app/unraid-api/cli/log.service';
+import { execa } from 'execa';
api/src/unraid-api/cli/logs.command.ts (1)

23-35: Add process termination handling

The command should handle process termination gracefully.

     async run(_: string[], options?: LogsOptions): Promise<void> {
         const lines = options?.lines ?? 100;
         const subprocess = execa(PM2_PATH, ['logs', ECOSYSTEM_PATH, '--lines', lines.toString()]);
+        
+        // Handle process termination
+        const cleanup = () => {
+            subprocess.kill();
+        };
+        process.on('SIGINT', cleanup);
+        process.on('SIGTERM', cleanup);
+        
         subprocess.stdout?.on('data', (data) => {
             this.logger.log(data.toString());
         });

         subprocess.stderr?.on('data', (data) => {
             this.logger.error(data.toString());
         });

-        await subprocess;
+        try {
+            await subprocess;
+        } finally {
+            process.off('SIGINT', cleanup);
+            process.off('SIGTERM', cleanup);
+        }
     }
api/src/unraid-api/cli/start.command.ts (1)

39-47: Simplify type casting in parseLogLevel method.

The current implementation has unnecessary type casting.

Apply this diff to simplify the type casting:

 @Option({
     flags: `--log-level <${levels.join('|')}>`,
     description: 'log level to use',
 })
 parseLogLevel(val: string): typeof levels {
-    return (levels.includes(val as (typeof levels)[number])
-        ? (val as (typeof levels)[number])
-        : 'info') as unknown as typeof levels;
+    return levels.includes(val as (typeof levels)[number])
+        ? val as (typeof levels)[number]
+        : 'info';
 }
api/src/types/my-servers-config.ts (1)

15-29: Enhance validation in RemoteConfigSchema.

The current schema could benefit from stronger validation rules for specific fields:

  1. wanport should be validated as a valid port number
  2. email should be validated as a valid email format
  3. upnpEnabled should be a boolean instead of string

Apply this diff to enhance validation:

 const RemoteConfigSchema = z.object({
     wanaccess: z.string(),
-    wanport: z.string(),
+    wanport: z.string().regex(/^\d+$/).transform(Number)
+        .refine(port => port >= 1 && port <= 65535, {
+            message: 'Port must be between 1 and 65535'
+        }),
     upnpEnabled: z.string(),
     apikey: z.string(),
     localApiKey: z.string(),
-    email: z.string(),
+    email: z.string().email(),
     username: z.string(),
     avatar: z.string(),
     regWizTime: z.string(),
     accesstoken: z.string(),
     idtoken: z.string(),
     refreshtoken: z.string(),
     dynamicRemoteAccessType: z.nativeEnum(DynamicRemoteAccessType),
 });
api/src/unraid-api/rest/rest.service.ts (1)

18-27: Improve error handling in saveApiReport.

The current implementation catches all errors and logs them generically. It would be better to handle specific error types differently.

Apply this diff to improve error handling:

 async saveApiReport(pathToReport: string) {
     try {
         const reportCommand = new ReportCommand(new LogService());
         const apiReport = await reportCommand.report({ json: true, verbose: 2, raw: false });
         this.logger.debug('Report object %o', apiReport);
         await writeFile(pathToReport, JSON.stringify(apiReport, null, 2), 'utf-8');
     } catch (error) {
-        this.logger.warn('Could not generate report for zip with error %o', error);
+        if (error instanceof Error) {
+            this.logger.warn('Could not generate report: %s', error.message);
+            throw error;
+        }
+        this.logger.warn('Could not generate report due to unknown error: %o', error);
+        throw new Error('Failed to generate report');
     }
 }
api/README.md (3)

46-66: Add language specifiers to code blocks.

Add 'bash' as the language specifier to the code blocks for better syntax highlighting and markdown compliance.

-```
+```bash
 unraid-api key get --name "my-app-key"
Apply similar changes to all code blocks in this section.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Markdownlint (0.37.0)</summary>

46-46: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

52-52: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

58-58: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

64-64: null
Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

---

`70-71`: **Fix grammar in the mothership connection description.**

Add the missing article "the" before "mothership".

```diff
-To view the current status of the unraid-api and its connection to mothership, run:
+To view the current status of the unraid-api and its connection to the mothership, run:
🧰 Tools
🪛 LanguageTool

[uncategorized] ~70-~70: You might be missing the article “the” here.
Context: ...of the unraid-api and its connection to mothership, run: unraid-api report To vi...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)


89-90: Fix punctuation in the Secrets section.

Add missing commas for better readability.

-If you found this file you're likely a developer. If you'd like to know more about the API and when it's available please join [our discord](https://discord.unraid.net/).
+If you found this file, you're likely a developer. If you'd like to know more about the API and when it's available, please join [our discord](https://discord.unraid.net/).
🧰 Tools
🪛 LanguageTool

[uncategorized] ~90-~90: Possible missing comma found.
Context: ... -vv ``` ## Secrets If you found this file you're likely a developer. If you'd lik...

(AI_HYDRA_LEO_MISSING_COMMA)


[typographical] ~90-~90: It seems that a comma is missing.
Context: ...re about the API and when it's available please join [our discord](https://discord.unra...

(IF_PLEASE_COMMA)

🛑 Comments failed to post (13)
api/src/cli.ts (1)

12-19: ⚠️ Potential issue

Trim trailing newline from shellToUse to prevent potential errors

The output of execSync('which bash') may contain a trailing newline character. If not trimmed, this can lead to issues when executablePath is used, as the path might not be recognized due to the newline. It's advisable to trim the output before using it.

Apply this diff to fix the issue:

 const shellToUse = execSync('which bash');
+const shellPath = shellToUse.toString('utf-8').trim();
 await CommandFactory.run(CliModule, {
     cliName: 'unraid-api',
     logger: false,
     completion: {
         fig: true,
         cmd: 'unraid-api',
-        nativeShell: { executablePath: shellToUse.toString('utf-8') },
+        nativeShell: { executablePath: shellPath },
     },
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const shellToUse = execSync('which bash');
    const shellPath = shellToUse.toString('utf-8').trim();
    await CommandFactory.run(CliModule, {
        cliName: 'unraid-api',
        logger: false,
        completion: {
            fig: true,
            cmd: 'unraid-api',
            nativeShell: { executablePath: shellPath },
api/src/unraid-api/cli/validate-token.command.ts (2)

65-65: ⚠️ Potential issue

Ensure token payload contains username field

When extracting the username from tokenPayload, it's possible that neither username nor cognito:username properties exist. Add a check to handle this scenario and provide an appropriate error message if the username cannot be retrieved.

Apply this diff to add the necessary check:

 const username = tokenPayload!.username ?? tokenPayload!['cognito:username'];
+if (!username) {
+    this.logger.error(createJsonErrorString('Username not found in token payload'));
+    process.exit(1);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        const username = tokenPayload!.username ?? tokenPayload!['cognito:username'];
        if (!username) {
            this.logger.error(createJsonErrorString('Username not found in token payload'));
            process.exit(1);
        }

71-79: ⚠️ Potential issue

Handle potential exceptions when decoding existing user token

The decodeJwt function may encounter issues if configFile.remote?.accesstoken is undefined or invalid, leading to unexpected behavior. Ensure that accesstoken exists before decoding and verify that existingUserPayload.username is present before proceeding with the comparison.

Apply this diff to add necessary checks:

 const existingUserPayload = configFile.remote?.accesstoken
+    ? decodeJwt(configFile.remote.accesstoken)
+    : null;
+if (!existingUserPayload || !existingUserPayload.username) {
+    this.logger.error(createJsonErrorString('Failed to decode existing user token or username not found'));
+    process.exit(1);
+}
 if (username === existingUserPayload.username) {
     this.logger.info(JSON.stringify({ error: null, valid: true }));
 } else {
     this.logger.error(
-        createJsonErrorString('Username on token does not match logged in user name')
+        createJsonErrorString('Username in token does not match logged-in user name')
     );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        const existingUserPayload = configFile.remote?.accesstoken
            ? decodeJwt(configFile.remote.accesstoken)
            : null;
        if (!existingUserPayload || !existingUserPayload.username) {
            this.logger.error(createJsonErrorString('Failed to decode existing user token or username not found'));
            process.exit(1);
        }
        if (username === existingUserPayload.username) {
            this.logger.info(JSON.stringify({ error: null, valid: true }));
        } else {
            this.logger.error(
                createJsonErrorString('Username in token does not match logged-in user name')
            );
        }
    }
api/src/unraid-api/cli/switch-env.command.ts (1)

46-47: ⚠️ Potential issue

Handle undefined values when parsing the environment file.

In the getEnvironmentFromFile method, currentEnvInFile might be undefined if the regex does not match. Passing undefined to parseStringToEnv can cause unexpected results. Consider providing a default value or adjusting the method to handle undefined.

Apply this diff to ensure currentEnvInFile is a string:

const [, , currentEnvInFile] = matchArray && matchArray.length === 3 ? matchArray : [];
- return this.parseStringToEnv(currentEnvInFile);
+ return this.parseStringToEnv(currentEnvInFile ?? '');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        const [, , currentEnvInFile] = matchArray && matchArray.length === 3 ? matchArray : [];
        return this.parseStringToEnv(currentEnvInFile ?? '');
api/src/unraid-api/cli/key.command.ts (2)

71-71: 🛠️ Refactor suggestion

Inject ApiKeyService via dependency injection instead of direct instantiation.

Directly instantiating services bypasses the benefits of NestJS's dependency injection system, such as lifecycle management and testability. Inject ApiKeyService in the constructor instead.

Modify the constructor to include ApiKeyService:

constructor(
    private readonly apiKeyService: ApiKeyService
) {
    super();
}

Update line 71 to use the injected service:

-        const apiKeyService = new ApiKeyService();

Don't forget to register ApiKeyService as a provider in your module.


99-100: 🛠️ Refactor suggestion

Avoid using process.exit(1) in the code.

Calling process.exit(1) can terminate the Node.js process abruptly, which might prevent proper cleanup. Consider throwing an error or allowing the process to complete gracefully.

Apply this diff:

-            process.exit(1);
+            throw new Error('No Key Found');

Ensure that any errors thrown are appropriately caught and handled at a higher level if necessary.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            throw new Error('No Key Found');
        }
api/src/unraid-api/cli/status.command.ts (1)

9-12: ⚠️ Potential issue

Replace blocking execSync with async execution and add error handling.

The current implementation has several issues:

  1. execSync blocks the event loop and can freeze the application
  2. Forced process.exit(0) bypasses proper cleanup
  3. Missing error handling for PM2 command failures

Consider this safer implementation:

-    async run(): Promise<void> {
-        execSync(`${PM2_PATH} status unraid-api`, { stdio: 'inherit' });
-        process.exit(0);
-    }
+    async run(): Promise<void> {
+        try {
+            const { execa } = await import('execa');
+            const { stdout, stderr } = await execa(PM2_PATH, ['status', 'unraid-api']);
+            if (stderr) {
+                console.error(stderr);
+                return;
+            }
+            console.log(stdout);
+        } catch (error) {
+            console.error('Failed to check service status:', error.message);
+            throw error;
+        }
+    }

Committable suggestion skipped: line range outside the PR's diff.

api/src/unraid-api/cli/report.spec.ts (1)

12-18: 🛠️ Refactor suggestion

Add more test cases for edge scenarios.

The current tests cover basic scenarios but miss important edge cases.

Consider adding these test cases:

test('anonymise origins handles empty array', () => {
    expect(anonymiseOrigins([])).toEqual([]);
});

test('anonymise origins handles invalid URLs', () => {
    expect(anonymiseOrigins(['invalid-url'])).toEqual(['invalid-url']);
});

test('anonymise origins handles multiple URLs', () => {
    expect(anonymiseOrigins([
        'https://domain1.tld:8443',
        'https://domain2.tld:9443'
    ])).toEqual([
        'https://domain1.tld:WANPORT',
        'https://domain2.tld:WANPORT'
    ]);
});
api/src/unraid-api/cli/stop.command.ts (1)

14-22: 🛠️ Refactor suggestion

Improve error handling and add timeout protection.

The current implementation has inconsistent error handling and no timeout protection.

Consider this improved implementation:

-    async run() {
-        const { stderr, stdout } = await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH]);
-        if (stdout) {
-            this.logger.info(stdout);
-        } else if (stderr) {
-            this.logger.warn(stderr);
-            process.exit(1);
-        }
-    }
+    async run() {
+        try {
+            const { stderr, stdout } = await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH], {
+                timeout: 30000, // 30 seconds timeout
+            });
+            
+            if (stderr) {
+                this.logger.warn(stderr);
+            }
+            
+            if (stdout) {
+                this.logger.info(stdout);
+            }
+            
+            // Verify the service is actually stopped
+            const { stdout: statusOut } = await execa(PM2_PATH, ['status', 'unraid-api']);
+            if (statusOut.includes('online')) {
+                throw new Error('Service failed to stop');
+            }
+        } catch (error) {
+            this.logger.error(`Failed to stop service: ${error.message}`);
+            throw error;
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    async run() {
        try {
            const { stderr, stdout } = await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH], {
                timeout: 30000, // 30 seconds timeout
            });
            
            if (stderr) {
                this.logger.warn(stderr);
            }
            
            if (stdout) {
                this.logger.info(stdout);
            }
            
            // Verify the service is actually stopped
            const { stdout: statusOut } = await execa(PM2_PATH, ['status', 'unraid-api']);
            if (statusOut.includes('online')) {
                throw new Error('Service failed to stop');
            }
        } catch (error) {
            this.logger.error(`Failed to stop service: ${error.message}`);
            throw error;
        }
    }
api/src/unraid-api/cli/restart.command.ts (1)

21-36: ⚠️ Potential issue

Replace execSync with async execution and add error handling

Using execSync blocks the Node.js event loop. Consider these critical improvements:

  1. Use async execution
  2. Add proper error handling
  3. Use the injected LogService instead of console.log
+    constructor(private readonly logger: LogService) {
+        super();
+    }
+
     async run(_): Promise<void> {
-        console.log(
-            'Dirname is ',
-            import.meta.dirname,
-            ' command is ',
-            `${PM2_PATH} restart ${ECOSYSTEM_PATH} --update-env`
-        );
-        execSync(
-            `${PM2_PATH} restart ${ECOSYSTEM_PATH} --update-env`,
-            {
-                env: process.env,
-                stdio: 'pipe',
-                cwd: process.cwd(),
-            }
-        );
+        try {
+            this.logger.info(`Executing: ${PM2_PATH} restart ${ECOSYSTEM_PATH} --update-env`);
+            const subprocess = execa(
+                PM2_PATH,
+                ['restart', ECOSYSTEM_PATH, '--update-env'],
+                {
+                    env: process.env,
+                    cwd: process.cwd(),
+                }
+            );
+            
+            subprocess.stdout?.pipe(process.stdout);
+            subprocess.stderr?.pipe(process.stderr);
+            
+            await subprocess;
+        } catch (error) {
+            this.logger.error(`Failed to restart: ${error.message}`);
+            throw error;
+        }
     }

Committable suggestion skipped: line range outside the PR's diff.

api/src/unraid-api/cli/logs.command.ts (1)

17-21: 🛠️ Refactor suggestion

Improve input validation in parseLines method

The current implementation has several issues:

  1. Uses unsafe isNaN instead of Number.isNaN
  2. Doesn't validate for negative numbers
  3. Doesn't validate maximum line limit
     @Option({ flags: '-l, --lines', description: 'Number of lines to tail'})
     parseLines(input: string): number
     {
-        return isNaN(parseInt(input)) ? 100 : parseInt(input)
+        const lines = Number(input);
+        if (Number.isNaN(lines) || lines < 1) {
+            return 100;
+        }
+        return Math.min(lines, 10000); // Add reasonable maximum limit
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    @Option({ flags: '-l, --lines', description: 'Number of lines to tail'})
    parseLines(input: string): number
    {
        const lines = Number(input);
        if (Number.isNaN(lines) || lines < 1) {
            return 100;
        }
        return Math.min(lines, 10000); // Add reasonable maximum limit
    }
🧰 Tools
🪛 Biome (1.9.4)

[error] 20-20: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

api/src/unraid-api/cli/start.command.ts (1)

21-37: ⚠️ Potential issue

Improve error handling in the run method.

The current implementation has a few issues:

  1. The process exits immediately after stdout, potentially missing stderr.
  2. The error handling could be more robust.

Apply this diff to improve error handling:

 async run(_, options: StartCommandOptions): Promise<void> {
     this.logger.info('Starting the Unraid API');
     const envLog = options['log-level'] ? `LOG_LEVEL=${options['log-level']}` : '';
-    const { stderr, stdout } = await execa(
-        `${envLog} ${PM2_PATH}`.trim(),
-        ['start', ECOSYSTEM_PATH, '--update-env'],
-        { stdio: 'inherit' }
-    );
-    if (stdout) {
-        this.logger.log(stdout);
-        process.exit(0);
-    }
-    if (stderr) {
-        this.logger.error(stderr);
-        process.exit(1);
-    }
+    try {
+        const { stderr, stdout } = await execa(
+            `${envLog} ${PM2_PATH}`.trim(),
+            ['start', ECOSYSTEM_PATH, '--update-env'],
+            { stdio: 'inherit' }
+        );
+        if (stderr) {
+            this.logger.error(stderr);
+            process.exit(1);
+        }
+        if (stdout) {
+            this.logger.log(stdout);
+        }
+        process.exit(0);
+    } catch (error) {
+        this.logger.error('Failed to start Unraid API:', error);
+        process.exit(1);
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    async run(_, options: StartCommandOptions): Promise<void> {
        this.logger.info('Starting the Unraid API');
        const envLog = options['log-level'] ? `LOG_LEVEL=${options['log-level']}` : '';
        try {
            const { stderr, stdout } = await execa(
                `${envLog} ${PM2_PATH}`.trim(),
                ['start', ECOSYSTEM_PATH, '--update-env'],
                { stdio: 'inherit' }
            );
            if (stderr) {
                this.logger.error(stderr);
                process.exit(1);
            }
            if (stdout) {
                this.logger.log(stdout);
            }
            process.exit(0);
        } catch (error) {
            this.logger.error('Failed to start Unraid API:', error);
            process.exit(1);
        }
    }
api/src/types/my-servers-config.ts (1)

58-60: 🛠️ Refactor suggestion

Remove redundant connectionStatus field.

The MyServersConfigMemoryWithMandatoryHiddenFieldsSchema extends MyServersConfigMemorySchema but redundantly includes the same connectionStatus field that's already present in the parent schema.

Apply this diff to remove the redundancy:

 export const MyServersConfigMemoryWithMandatoryHiddenFieldsSchema = MyServersConfigMemorySchema.extend({
-    connectionStatus: ConnectionStatusSchema,
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const MyServersConfigMemoryWithMandatoryHiddenFieldsSchema = MyServersConfigMemorySchema.extend({
});

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL: https://preview.dl.unraid.net/unraid-api/pr/1072/dynamix.unraid.net.staging.plg

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL: https://preview.dl.unraid.net/unraid-api/pr/1072/dynamix.unraid.net.staging.plg

@elibosley elibosley merged commit e6e3c53 into feat/add-cli-command-for-api-keys Jan 23, 2025
11 checks passed
@elibosley elibosley deleted the feat/zod-config branch January 23, 2025 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants