Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/dev/configs/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"https://test.com"
],
"sandbox": true,
"ssoSubIds": []
"ssoSubIds": [],
"plugins": []
}
77 changes: 76 additions & 1 deletion api/src/__test__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { csvStringToArray, formatDatetime } from '@app/utils.js';
import { csvStringToArray, formatDatetime, parsePackageArg } from '@app/utils.js';

describe('formatDatetime', () => {
const testDate = new Date('2024-02-14T12:34:56');
Expand Down Expand Up @@ -103,3 +103,78 @@ describe('csvStringToArray', () => {
expect(csvStringToArray(',one,')).toEqual(['one']);
});
});

describe('parsePackageArg', () => {
it('parses simple package names without version', () => {
expect(parsePackageArg('lodash')).toEqual({ name: 'lodash' });
expect(parsePackageArg('express')).toEqual({ name: 'express' });
expect(parsePackageArg('react')).toEqual({ name: 'react' });
});

it('parses simple package names with version', () => {
expect(parsePackageArg('lodash@4.17.21')).toEqual({ name: 'lodash', version: '4.17.21' });
expect(parsePackageArg('express@4.18.2')).toEqual({ name: 'express', version: '4.18.2' });
expect(parsePackageArg('react@18.2.0')).toEqual({ name: 'react', version: '18.2.0' });
});

it('parses scoped package names without version', () => {
expect(parsePackageArg('@types/node')).toEqual({ name: '@types/node' });
expect(parsePackageArg('@angular/core')).toEqual({ name: '@angular/core' });
expect(parsePackageArg('@nestjs/common')).toEqual({ name: '@nestjs/common' });
});

it('parses scoped package names with version', () => {
expect(parsePackageArg('@types/node@18.15.0')).toEqual({
name: '@types/node',
version: '18.15.0',
});
expect(parsePackageArg('@angular/core@15.2.0')).toEqual({
name: '@angular/core',
version: '15.2.0',
});
expect(parsePackageArg('@nestjs/common@9.3.12')).toEqual({
name: '@nestjs/common',
version: '9.3.12',
});
});

it('handles version ranges and tags', () => {
expect(parsePackageArg('lodash@^4.17.0')).toEqual({ name: 'lodash', version: '^4.17.0' });
expect(parsePackageArg('react@~18.2.0')).toEqual({ name: 'react', version: '~18.2.0' });
expect(parsePackageArg('express@latest')).toEqual({ name: 'express', version: 'latest' });
expect(parsePackageArg('vue@beta')).toEqual({ name: 'vue', version: 'beta' });
expect(parsePackageArg('@types/node@next')).toEqual({ name: '@types/node', version: 'next' });
});

it('handles multiple @ symbols correctly', () => {
expect(parsePackageArg('package@1.0.0@extra')).toEqual({
name: 'package@1.0.0',
version: 'extra',
});
expect(parsePackageArg('@scope/pkg@1.0.0@extra')).toEqual({
name: '@scope/pkg@1.0.0',
version: 'extra',
});
});

it('ignores versions that contain forward slashes', () => {
expect(parsePackageArg('package@github:user/repo')).toEqual({
name: 'package@github:user/repo',
});
expect(parsePackageArg('@scope/pkg@git+https://github.com/user/repo.git')).toEqual({
name: '@scope/pkg@git+https://github.com/user/repo.git',
});
});

it('handles edge cases', () => {
expect(parsePackageArg('@')).toEqual({ name: '@' });
expect(parsePackageArg('@scope')).toEqual({ name: '@scope' });
expect(parsePackageArg('package@')).toEqual({ name: 'package@' });
expect(parsePackageArg('@scope/pkg@')).toEqual({ name: '@scope/pkg@' });
});

it('handles empty version strings', () => {
expect(parsePackageArg('package@')).toEqual({ name: 'package@' });
expect(parsePackageArg('@scope/package@')).toEqual({ name: '@scope/package@' });
});
});
48 changes: 48 additions & 0 deletions api/src/unraid-api/app/dependency.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import * as path from 'path';

import { execa } from 'execa';

import { fileExists } from '@app/core/utils/files/file-exists.js';
import { getPackageJsonPath } from '@app/environment.js';

@Injectable()
export class DependencyService {
constructor() {}

/**
* Executes an npm command.
*
* @param npmArgs - The arguments to pass to npm.
* @returns The execa result of the npm command.
*/
async npm(...npmArgs: string[]) {
return await execa('npm', [...npmArgs], {
stdio: 'inherit',
cwd: path.dirname(getPackageJsonPath()),
});
}

/**
* Installs dependencies for the api using npm.
*
* @throws {Error} from execa if the npm install command fails.
*/
async npmInstall(): Promise<void> {
await this.npm('install');
}

/**
* Rebuilds the vendored dependency archive for the api and stores it on the boot drive.
* If the rc.unraid-api script is not found, an error is thrown.
*
* @throws {Error} from execa if the rc.unraid-api command fails.
*/
async rebuildVendorArchive(): Promise<void> {
const rcUnraidApi = '/etc/rc.d/rc.unraid-api';
if (!(await fileExists(rcUnraidApi))) {
throw new Error('[rebuild-vendor-archive] rc.unraid-api not found; no action taken!');
}
await execa(rcUnraidApi, ['archive-dependencies'], { stdio: 'inherit' });
}
}
25 changes: 18 additions & 7 deletions api/src/unraid-api/cli/cli.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';

import { DependencyService } from '@app/unraid-api/app/dependency.service.js';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { SsoUserService } from '@app/unraid-api/auth/sso-user.service.js';
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
Expand All @@ -10,7 +11,12 @@ import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.comman
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { LogsCommand } from '@app/unraid-api/cli/logs.command.js';
import { PluginCommandModule } from '@app/unraid-api/cli/plugins/plugin.cli.module.js';
import {
InstallPluginCommand,
ListPluginCommand,
PluginCommand,
RemovePluginCommand,
} from '@app/unraid-api/cli/plugins/plugin.command.js';
import { PM2Service } from '@app/unraid-api/cli/pm2.service.js';
import { ReportCommand } from '@app/unraid-api/cli/report.command.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
Expand All @@ -30,26 +36,30 @@ import { ApiConfigModule } from '@app/unraid-api/config/api-config.module.js';
import { LegacyConfigModule } from '@app/unraid-api/config/legacy-config.module.js';
import { PluginCliModule } from '@app/unraid-api/plugin/plugin.module.js';

// cli - plugin add/remove
// plugin generator

const DEFAULT_COMMANDS = [
ApiKeyCommand,
ConfigCommand,
DeveloperCommand,
LogsCommand,
ReportCommand,
VersionCommand,
// Lifecycle commands
SwitchEnvCommand,
RestartCommand,
StartCommand,
StatusCommand,
StopCommand,
SwitchEnvCommand,
VersionCommand,
// SSO commands
SSOCommand,
ValidateTokenCommand,
AddSSOUserCommand,
RemoveSSOUserCommand,
ListSSOUserCommand,
// Plugin commands
PluginCommand,
ListPluginCommand,
InstallPluginCommand,
RemovePluginCommand,
] as const;

const DEFAULT_PROVIDERS = [
Expand All @@ -62,10 +72,11 @@ const DEFAULT_PROVIDERS = [
PM2Service,
ApiKeyService,
SsoUserService,
DependencyService,
] as const;

@Module({
imports: [LegacyConfigModule, ApiConfigModule, PluginCliModule.register(), PluginCommandModule],
imports: [LegacyConfigModule, ApiConfigModule, PluginCliModule.register()],
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS],
})
export class CliModule {}
6 changes: 6 additions & 0 deletions api/src/unraid-api/cli/log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export class LogService {
return shouldLog;
}

table(level: LogLevel, data: unknown, columns?: string[]) {
if (this.shouldLog(level)) {
console.table(data, columns);
}
}

log(...messages: unknown[]): void {
if (this.shouldLog('info')) {
this.logger.log(...messages);
Expand Down
131 changes: 0 additions & 131 deletions api/src/unraid-api/cli/plugins/dependency.service.ts

This file was deleted.

28 changes: 0 additions & 28 deletions api/src/unraid-api/cli/plugins/plugin.cli.module.ts

This file was deleted.

Loading
Loading