hydrooj-addons-manager 0.0.1 → 0.0.2-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { HydroResponse } from '@hydrooj/framework';
2
1
  import {
3
2
  Context, Handler, PRIV, Schema,
4
3
  Service, superagent, SystemModel,
@@ -7,31 +6,44 @@ import {
7
6
  Model,
8
7
  requireSudo
9
8
  } from 'hydrooj';
10
- import { exec } from 'child_process';
11
- import * as fs from 'fs';
9
+ import { exec, spawn } from 'child_process';
10
+ import * as fs from 'fs/promises';
11
+ import * as path from 'path';
12
+ import semver from 'semver';
13
+ import validatePackageName from 'validate-npm-package-name';
14
+ type CommandResult = {success: boolean, message?: string};
15
+ type PackageInfo = {name: string, version: string};
16
+ type PackageAction = 'add' | 'delete' | 'update';
17
+ type PackageOperation = PackageInfo & {action: PackageAction};
12
18
 
13
- let addonsManagerModel : {
14
- add: (name: string, version?: string) => Promise<commandResult>,
15
- update: (name: string, version?: string) => Promise<commandResult>,
16
- remove: (name: string) => Promise<commandResult>,
17
- getActivedPackages: () => Promise<String[]>,
18
- getLockedPackages: () => Promise<String[]>
19
+ type AddonsManagerModel = {
20
+ manageAddon: (action: PackageAction, name: string, version?: string) => Promise<CommandResult>,
21
+ localUpdate: (name: string) => Promise<CommandResult>,
22
+ getActivedPackages: () => Promise<string[]>,
23
+ getLockedPackages: () => Promise<string[]>
19
24
  };
20
- type commandResult = {success: boolean, message?: string};
21
- function checkNpmPackageValidity(name : string): Boolean {
22
- return /^(?:(?:@(?:[a-z0-9-*~][a-z0-9-*. _~]*)?\/[a-z0-9-._~])|[a-z0-9-~])[a-z0-9-._~]*$/.test(name);
25
+ function checkNpmPackageValidity(name : string): boolean {
26
+ return validatePackageName(name).validForNewPackages;
23
27
  }
24
- function checkNpmVersionValidity(name : string): Boolean {
25
- return name === '' || /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/.test(name);
28
+ function checkNpmVersionValidity(name : string): boolean {
29
+ return name === '' || semver.valid(name) !== null;
30
+ }
31
+ function checkPackageIsLocal(name : string): boolean {
32
+ return name[0] === '/' && path.isAbsolute(name);
26
33
  }
27
34
  class AddonsManagerHandler extends Handler {
35
+ private static model: AddonsManagerModel;
36
+ static setModel(m: AddonsManagerModel) { this.model = m; }
28
37
  @requireSudo
29
38
  async get()
30
39
  {
31
40
  this.response.template = 'manage_addons.html';
32
- const packages = await addonsManagerModel.getActivedPackages();
33
- const lockedPackages = await addonsManagerModel.getLockedPackages();
34
- this.response.body = {
41
+ const packages = await AddonsManagerHandler.model.getActivedPackages();
42
+ let lockedPackages = await AddonsManagerHandler.model.getLockedPackages();
43
+ for(const pkg of packages){
44
+ if(checkPackageIsLocal(pkg)) lockedPackages.push(pkg);
45
+ }
46
+ this.response.body = this.request.body ||{
35
47
  packages: packages,
36
48
  lockedPackages: lockedPackages,
37
49
  result: null
@@ -41,23 +53,27 @@ class AddonsManagerHandler extends Handler {
41
53
  async post()
42
54
  {
43
55
  const body = this.request.body;
44
- let result: commandResult = {success: false, message: 'Unknown error'};
45
- if(body.state === 'activate'){
46
- const name = body.new_package_name;
47
- const version = body.new_package_version;
48
- result = await addonsManagerModel.add(name, version);
49
- }else if(body['delete_package']){
50
- const name = body['delete_package'];
51
- result = await addonsManagerModel.remove(name);
52
- }else if(body['update_package']){
53
- const name = body['update_package'];
54
- result = await addonsManagerModel.update(name);
55
- } else{
56
- throw new UserFacingError('Invalid request');
56
+ const packages = await AddonsManagerHandler.model.getActivedPackages();
57
+ let lockedPackages = await AddonsManagerHandler.model.getLockedPackages();
58
+ for(const pkg of packages){
59
+ if(checkPackageIsLocal(pkg)) lockedPackages.push(pkg);
57
60
  }
58
- const packages = await addonsManagerModel.getActivedPackages();
59
- const lockedPackages = await addonsManagerModel.getLockedPackages();
60
- console.log(result);
61
+
62
+ let pkg: PackageOperation =
63
+ {
64
+ name: body['package_name'],
65
+ version: body['package_version'] || '',
66
+ action: body['action']
67
+ };
68
+
69
+ let result: CommandResult = {success: false, message: 'Unknown error'};
70
+ if(!(pkg.name in packages) && pkg.action === 'add')result = {success: false, message: 'Package is not installed'};
71
+ if(checkPackageIsLocal(pkg.name))
72
+ {
73
+ if(pkg.action !== 'update') result = {success: false, message: 'Local packages can only be updated'};
74
+ else result = await AddonsManagerHandler.model.localUpdate(pkg.name);
75
+ }else result = await AddonsManagerHandler.model.manageAddon(pkg.action, pkg.name, pkg.version);
76
+
61
77
  this.back({
62
78
  result: result,
63
79
  packages: packages,
@@ -66,26 +82,21 @@ class AddonsManagerHandler extends Handler {
66
82
  }
67
83
  }
68
84
 
69
- async function sendCommand(cmd: string, cwd: string) : Promise<commandResult> {
85
+ async function sendCommand(command: string, cwd: string): Promise<CommandResult> {
70
86
  return new Promise((resolve) => {
71
- try {
72
- exec(cmd, { cwd: cwd }, (err, stdout, stderr) => {
73
- if (err) {
74
- console.error(`exec error: ${err}`);
75
- resolve({ success: false, message: err.message });
76
- return;
77
- }
78
- if (stderr) {
79
- console.error(`stderr: ${stderr}`);
80
- resolve({ success: false, message: stderr });
81
- return;
82
- }
83
- console.log(`stdout: ${stdout}`);
84
- resolve({ success: true });
85
- });
86
- } catch (error) {
87
- resolve({ success: false, message: (error as Error).message });
88
- }
87
+ const child = spawn(command, { cwd });
88
+ let stdout = '';
89
+ let stderr = '';
90
+ child.stdout?.on('data', (data) => { stdout += data.toString(); });
91
+ child.stderr?.on('data', (data) => { stderr += data.toString(); });
92
+ child.on('error', (error) => resolve({ success: false, message: error.message }));
93
+ child.on('close', (code) => {
94
+ if (code !== 0) {
95
+ resolve({ success: false, message: stderr || stdout || `Exit code ${code}` });
96
+ } else {
97
+ resolve({ success: true, message: stdout || stderr });
98
+ }
99
+ });
89
100
  });
90
101
  }
91
102
 
@@ -100,54 +111,57 @@ export default class AddonsManagerService extends Service {
100
111
  global.Hydro.ui.inject('ControlPanel', 'manage_addons');
101
112
  init();
102
113
  async function init() {
114
+ // Model functions
115
+ async function manageAddon(action: PackageAction, name: string, version: string = ''): Promise<CommandResult> {
116
+ if (!checkNpmPackageValidity(name)) return {success: false, message: 'Invalid package name'};
117
+ if (version !== '' && !checkNpmVersionValidity(version)) return {success: false, message: 'Invalid version'};
103
118
 
104
- async function remove(name: string): Promise<commandResult> {
105
- if(!checkNpmPackageValidity(name))throw new UserFacingError('Invalid package name');
106
- await sendCommand("yarn global remove "+name, config.pathToHydro);
107
- const result = await sendCommand("hydrooj addon remove "+name, config.pathToHydro);
108
- return result;
109
- }
110
- async function update(name: string, version: string = ''): Promise<commandResult> {
111
- if(!checkNpmPackageValidity(name))throw new UserFacingError('Invalid package name');
112
- if(!checkNpmVersionValidity(version))throw new UserFacingError('Invalid version');
113
- const result = await sendCommand("yarn global add "+name+(version ? '@' + version : ''), config.pathToHydro);
119
+ let result: CommandResult = {success: false, message: 'Unknown error'};
120
+ switch (action) {
121
+ case 'delete':
122
+ await sendCommand("yarn global remove " + name, config.pathToHydro);
123
+ result = await sendCommand("hydrooj addon remove " + name, config.pathToHydro);
124
+ break;
125
+ case 'update':
126
+ result = await sendCommand("yarn global upgrade " + name + (version ? '@' + version : ''), config.pathToHydro);
127
+ break;
128
+ case 'add':
129
+ result = await sendCommand("yarn global add " + name + (version ? '@' + version : ''), config.pathToHydro);
130
+ result = await sendCommand("hydrooj addon add " + name, config.pathToHydro);
131
+ break;
132
+ }
114
133
  return result;
115
134
  }
116
- async function add(name: string, version: string = ''): Promise<commandResult> {
117
- if(!checkNpmPackageValidity(name))throw new UserFacingError('Invalid package name');
118
- if(!checkNpmVersionValidity(version))throw new UserFacingError('Invalid version');
119
- const updateResult = await update(name, version);
120
- const result = await sendCommand("hydrooj addon add "+name+(version ? '@' + version : ''), config.pathToHydro);
135
+ async function localUpdate(name: string): Promise<CommandResult> {
136
+ if (!checkPackageIsLocal(name)) return {success: false, message: 'Not a local package'};
137
+ let result: CommandResult = {success: false, message: 'Unknown error'};
138
+ result = await sendCommand("git pull origin main", name);
121
139
  return result;
122
140
  }
123
-
124
-
125
- async function getActivedPackages(): Promise<String[]> {
126
- return new Promise((resolve, reject) => {
127
- let packages: String[] = [];
128
- fs.readFile(config.pathToHydro+'addon.json', (err, data) => {
129
- if(err) reject(new UserFacingError(err.message));
130
- else {
131
- packages = JSON.parse(data.toString());
132
- resolve(packages);
133
- }
134
- });
135
- });
141
+ async function getActivedPackages(): Promise<string[]> {
142
+ try {
143
+ const data = await fs.readFile(config.pathToHydro+'addon.json', 'utf-8');
144
+ return JSON.parse(data);
145
+ } catch (err) {
146
+ return [];
147
+ }
136
148
  }
137
149
 
138
- async function getLockedPackages(): Promise<String[]> {
139
- return new Promise((resolve, reject) => {
140
- let packages: String[] = [];
141
- fs.readFile(config.pathToHydro+'addon-locked.json', (err, data) => {
142
- if(err) reject(new UserFacingError(err.message));
143
- else {
144
- packages = JSON.parse(data.toString());
145
- resolve(packages);
146
- }
147
- });
148
- });
150
+ async function getLockedPackages(): Promise<string[]> {
151
+ try {
152
+ const data = await fs.readFile(config.pathToHydro+'addon-locked.json', 'utf-8');
153
+ return JSON.parse(data);
154
+ } catch (err) {
155
+ return [];
156
+ }
149
157
  }
150
- addonsManagerModel = { add, update, remove, getActivedPackages, getLockedPackages };
158
+ const addonsManagerModel: AddonsManagerModel = {
159
+ manageAddon: manageAddon,
160
+ localUpdate: localUpdate,
161
+ getActivedPackages: () => getActivedPackages(),
162
+ getLockedPackages: () => getLockedPackages()
163
+ };
164
+ AddonsManagerHandler.setModel(addonsManagerModel);
151
165
  }
152
166
  }
153
167
  }
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "hydrooj-addons-manager",
3
- "version": "0.0.1",
3
+ "version": "0.0.2-dev",
4
4
  "description": "addons manager for hydrooj",
5
5
  "main": "index.ts",
6
- "author": "https://github.com/Bryan0324"
7
- }
6
+ "author": "https://github.com/Bryan0324",
7
+ "dependencies": {
8
+ "@types/semver": "^7.7.1",
9
+ "@types/validate-npm-package-name": "^4.0.2"
10
+ },
11
+ "devDependencies": {
12
+ "@types/semver": "^7.7.1",
13
+ "@types/validate-npm-package-name": "^4.0.2"
14
+ }
15
+ }
@@ -109,10 +109,11 @@
109
109
  <form method="post" class="activate-package-form">
110
110
  <div class="activate-package-item">
111
111
  <a>{{ package }}</a>
112
+ <input type="text" name="package_name" value="{{ package }}" hidden >
112
113
  {% if package not in lockedPackages %}
113
- <button type="submit" name="delete_package" value="{{ package }}" class="btn btn--danger">{{ _('delete') }}</button>
114
+ <button type="submit" name="action" value="delete" class="btn btn--danger">{{ _('delete') }}</button>
114
115
  {% endif %}
115
- <button type="submit" name="update_package" value="{{ package }}" class="btn btn--secondary">{{ _('update') }}</button>
116
+ <button type="submit" name="action" value="update" class="btn btn--secondary">{{ _('update') }}</button>
116
117
  </div>
117
118
  </form>
118
119
  </li>
@@ -121,9 +122,9 @@
121
122
  <h2 class="section__subtitle" data-heading>{{ _('add new package') }}</h2>
122
123
  <form method="post" class="activate-package-form">
123
124
  <div class="activate-package-item">
124
- <input type="text" name="new_package_name" placeholder="{{ _('package name') }}" required>
125
- <input type="text" name="new_package_version" placeholder="{{ _('version name *unnecessary') }}">
126
- <button type="submit" name="state" value="activate" class="btn btn--primary">{{ _('activate') }}</button>
125
+ <input type="text" name="package_name" placeholder="{{ _('package name') }}" required>
126
+ <input type="text" name="package_version" placeholder="{{ _('version name *unnecessary') }}">
127
+ <button type="submit" name="action" value="add" class="btn btn--primary">{{ _('activate') }}</button>
127
128
  </div>
128
129
  </form>
129
130
  {% if result %}