@open-audio-stack/core 0.1.9 → 0.1.11

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.
@@ -5,8 +5,14 @@ import { ConfigInterface } from '../types/Config.js';
5
5
  export declare class ManagerLocal extends Manager {
6
6
  protected typeDir: string;
7
7
  constructor(type: RegistryType, config: ConfigInterface);
8
- scan(ext?: string): void;
8
+ create(): Promise<void>;
9
+ scan(ext?: string, installable?: boolean): void;
9
10
  export(dir: string, ext?: string): boolean;
10
11
  install(slug: string, version?: string): Promise<void | PackageVersion>;
12
+ installDependency(slug: string, version?: string, filePath?: string, type?: RegistryType): Promise<any>;
13
+ installDependencies(filePath?: string, type?: RegistryType): Promise<any>;
14
+ open(filePath?: string): void;
11
15
  uninstall(slug: string, version?: string): Promise<void | PackageVersion>;
16
+ uninstallDependency(slug: string, version?: string, filePath?: string, type?: RegistryType): Promise<any>;
17
+ uninstallDependencies(filePath?: string, type?: RegistryType): Promise<any>;
12
18
  }
@@ -1,8 +1,8 @@
1
1
  import path from 'path';
2
2
  import { Package } from './Package.js';
3
3
  import { Manager } from './Manager.js';
4
- import { archiveExtract, dirCreate, dirDelete, dirEmpty, dirRead, fileCreate, fileCreateJson, fileCreateYaml, fileExists, fileHash, fileOpen, fileReadJson, fileReadYaml, filesMove, isAdmin, runCliAsAdmin, } from '../helpers/file.js';
5
- import { pathGetSlug, pathGetVersion } from '../helpers/utils.js';
4
+ import { archiveExtract, dirCreate, dirDelete, dirEmpty, dirRead, fileCreate, fileCreateJson, fileCreateYaml, fileExists, fileHash, fileInstall, fileOpen, fileReadJson, fileReadYaml, filesMove, isAdmin, runCliAsAdmin, } from '../helpers/file.js';
5
+ import { isValidVersion, pathGetSlug, pathGetVersion, toSlug } from '../helpers/utils.js';
6
6
  import { commandExists, getArchitecture, getSystem, isTests } from '../helpers/utilsLocal.js';
7
7
  import { apiBuffer } from '../helpers/api.js';
8
8
  import { FileType } from '../types/FileType.js';
@@ -12,7 +12,14 @@ import { ConfigLocal } from './ConfigLocal.js';
12
12
  import { packageCompatibleFiles } from '../helpers/package.js';
13
13
  import { presetFormatDir } from '../types/PresetFormat.js';
14
14
  import { projectFormatDir } from '../types/ProjectFormat.js';
15
- import { FileFormat, SystemType } from '../index-browser.js';
15
+ import { FileFormat } from '../types/FileFormat.js';
16
+ import { licenses } from '../types/License.js';
17
+ import { pluginTypes } from '../types/PluginType.js';
18
+ import { presetTypes } from '../types/PresetType.js';
19
+ import { projectTypes } from '../types/ProjectType.js';
20
+ import { SystemType } from '../types/SystemType.js';
21
+ import { packageLoadFile, packageSaveFile } from '../helpers/packageLocal.js';
22
+ import inquirer from 'inquirer';
16
23
  export class ManagerLocal extends Manager {
17
24
  typeDir;
18
25
  constructor(type, config) {
@@ -21,12 +28,97 @@ export class ManagerLocal extends Manager {
21
28
  this.config = new ConfigLocal(configPath, config);
22
29
  this.typeDir = this.config.get(`${type}Dir`);
23
30
  }
24
- scan(ext = 'json') {
31
+ async create() {
32
+ // TODO Rewrite this code after prototype is proven.
33
+ const pkgQuestions = [
34
+ {
35
+ name: 'org',
36
+ type: 'input',
37
+ message: 'Org id',
38
+ default: 'org-name',
39
+ validate: (value) => value === toSlug(value),
40
+ },
41
+ {
42
+ name: 'package',
43
+ type: 'input',
44
+ message: 'Package id',
45
+ default: 'package-name',
46
+ validate: (value) => value === toSlug(value),
47
+ },
48
+ {
49
+ name: 'version',
50
+ type: 'input',
51
+ message: 'Package version',
52
+ default: '1.0.0',
53
+ validate: (value) => isValidVersion(value),
54
+ },
55
+ ];
56
+ const pkgAnswers = await inquirer.prompt(pkgQuestions);
57
+ let types = pluginTypes;
58
+ if (this.type === RegistryType.Presets) {
59
+ types = presetTypes;
60
+ }
61
+ else if (this.type === RegistryType.Projects) {
62
+ types = projectTypes;
63
+ }
64
+ const pkgVersionQuestions = [
65
+ { name: 'name', type: 'input', message: 'Package name' },
66
+ { name: 'author', type: 'input', message: 'Author name' },
67
+ { name: 'description', type: 'input', message: 'Description' },
68
+ { name: 'license', type: 'list', message: 'License', choices: licenses },
69
+ { name: 'type', type: 'list', message: 'Type', choices: types },
70
+ {
71
+ name: 'tags',
72
+ type: 'input',
73
+ message: 'Tags (comma-separated)',
74
+ filter: (input) => input
75
+ .split(',')
76
+ .map(tag => tag.trim())
77
+ .filter(tag => tag.length > 0),
78
+ },
79
+ {
80
+ name: 'url',
81
+ type: 'input',
82
+ message: 'Website url',
83
+ default: `https://github.com/${pkgAnswers.org}/${pkgAnswers.package}`,
84
+ },
85
+ {
86
+ name: 'audio',
87
+ type: 'input',
88
+ message: 'Audio preview url',
89
+ default: `https://open-audio-stack.github.io/open-audio-stack-registry/${this.type}/${pkgAnswers.org}/${pkgAnswers.package}/${pkgAnswers.package}.flac`,
90
+ },
91
+ {
92
+ name: 'image',
93
+ type: 'input',
94
+ message: 'Image preview url',
95
+ default: `https://open-audio-stack.github.io/open-audio-stack-registry/${this.type}/${pkgAnswers.org}/${pkgAnswers.package}/${pkgAnswers.package}.jpg`,
96
+ },
97
+ { name: 'date', type: 'input', message: 'Date released', default: new Date().toISOString() },
98
+ { name: 'changes', type: 'input', message: 'List of changes' },
99
+ ];
100
+ if (this.type === RegistryType.Projects) {
101
+ pkgVersionQuestions.push({ name: 'open', type: 'input', message: 'File to open' });
102
+ }
103
+ const pkgVersionAnswers = await inquirer.prompt(pkgVersionQuestions);
104
+ // TODO prompt for each file.
105
+ pkgVersionAnswers.files = [];
106
+ if (this.type === RegistryType.Presets || this.type === RegistryType.Projects) {
107
+ pkgVersionAnswers.plugins = [];
108
+ }
109
+ console.log(pkgVersionAnswers);
110
+ const pkg = new Package(`${pkgAnswers.org}/${pkgAnswers.package}`);
111
+ pkg.addVersion(pkgAnswers.version, pkgVersionAnswers);
112
+ console.log(JSON.stringify(pkg.getReport(), null, 2));
113
+ this.addPackage(pkg);
114
+ }
115
+ scan(ext = 'json', installable = true) {
25
116
  const filePaths = dirRead(`${this.typeDir}/**/index.${ext}`);
26
117
  filePaths.forEach((filePath) => {
27
118
  const subPath = filePath.replace(`${this.typeDir}/`, '');
28
119
  const pkgJson = ext === 'yaml' ? fileReadYaml(filePath) : fileReadJson(filePath);
29
- pkgJson.installed = true;
120
+ if (installable)
121
+ pkgJson.installed = true;
30
122
  const pkg = new Package(pathGetSlug(subPath));
31
123
  const version = pathGetVersion(subPath);
32
124
  pkg.addVersion(version, pkgJson);
@@ -42,6 +134,7 @@ export class ManagerLocal extends Manager {
42
134
  dirCreate(path.join(dir, pkgSlug, version));
43
135
  saveFile(path.join(dir, pkgSlug, version, filename), pkgVersion);
44
136
  }
137
+ dirCreate(path.join(dir, pkgSlug, filename));
45
138
  saveFile(path.join(dir, pkgSlug, filename), pkg.toJSON());
46
139
  // TODO find a more elegant way to handle org exports.
47
140
  const pkgOrg = pkgSlug.split('/')[0];
@@ -105,9 +198,12 @@ export class ManagerLocal extends Manager {
105
198
  const hash = await fileHash(filePath);
106
199
  if (hash !== file.sha256)
107
200
  return console.error(`Error: ${filePath} hash mismatch`);
108
- // If installer, run the installer.
201
+ // If installer, run the installer headless (without the user interface).
109
202
  if (file.type === FileType.Installer) {
110
- fileOpen(filePath);
203
+ if (isTests())
204
+ fileOpen(filePath);
205
+ else
206
+ fileInstall(filePath);
111
207
  }
112
208
  // If archive, extract the archive to temporary directory, then move individual files.
113
209
  if (file.type === FileType.Archive) {
@@ -130,6 +226,50 @@ export class ManagerLocal extends Manager {
130
226
  pkgVersion.installed = true;
131
227
  return pkgVersion;
132
228
  }
229
+ async installDependency(slug, version, filePath, type = RegistryType.Plugins) {
230
+ // Get dependency package information from registry.
231
+ const manager = new ManagerLocal(type, this.config.config);
232
+ await manager.sync();
233
+ const pkg = manager.getPackage(slug);
234
+ if (!pkg)
235
+ return console.error(`Package ${slug} not found in registry`);
236
+ const versionNum = version || pkg.latestVersion();
237
+ const pkgVersion = pkg?.getVersion(versionNum);
238
+ if (!pkgVersion)
239
+ return console.error(`Package ${slug} version ${versionNum} not found in registry`);
240
+ // Get local package file.
241
+ const pkgFile = packageLoadFile(filePath);
242
+ if (pkgFile[type] && pkgFile[type][slug] && pkgFile[type][slug] === versionNum) {
243
+ return console.error(`Package ${slug} version ${versionNum} is already a dependency`);
244
+ }
245
+ // Install dependency.
246
+ await manager.install(slug, version);
247
+ // Add dependency to local package file and save.
248
+ if (!pkgFile[type])
249
+ pkgFile[type] = {};
250
+ pkgFile[type][slug] = versionNum;
251
+ packageSaveFile(pkgFile, filePath);
252
+ pkgFile.installed = true;
253
+ return pkgFile;
254
+ }
255
+ async installDependencies(filePath, type = RegistryType.Plugins) {
256
+ // Loop through dependency packages and install each one.
257
+ const pkgFile = packageLoadFile(filePath);
258
+ const manager = new ManagerLocal(type, this.config.config);
259
+ await manager.sync();
260
+ for (const slug in pkgFile[type]) {
261
+ await manager.install(slug, pkgFile[type][slug]);
262
+ }
263
+ pkgFile.installed = true;
264
+ return pkgFile;
265
+ }
266
+ open(filePath) {
267
+ const pkgFile = packageLoadFile(filePath);
268
+ // If installer, run the installer.
269
+ if (pkgFile.open) {
270
+ fileOpen(pkgFile.open);
271
+ }
272
+ }
133
273
  async uninstall(slug, version) {
134
274
  // Get package information from registry.
135
275
  const pkg = this.getPackage(slug);
@@ -169,4 +309,34 @@ export class ManagerLocal extends Manager {
169
309
  delete pkgVersion.installed;
170
310
  return this.getPackage(slug)?.getVersion(versionNum);
171
311
  }
312
+ async uninstallDependency(slug, version, filePath, type = RegistryType.Plugins) {
313
+ // Get local package file.
314
+ const pkgFile = packageLoadFile(filePath);
315
+ if (!pkgFile[type])
316
+ return console.error(`Package ${type} is missing`);
317
+ if (!pkgFile[type][slug])
318
+ return console.error(`Package ${type} ${slug} is not a dependency`);
319
+ // Uninstall dependency.
320
+ const manager = new ManagerLocal(type, this.config.config);
321
+ await manager.sync();
322
+ await manager.uninstall(slug, version || pkgFile[type][slug]);
323
+ // Remove dependency from local package file and save.
324
+ if (!pkgFile[type])
325
+ pkgFile[type] = {};
326
+ delete pkgFile[type][slug];
327
+ packageSaveFile(pkgFile, filePath);
328
+ pkgFile.installed = true;
329
+ return pkgFile;
330
+ }
331
+ async uninstallDependencies(filePath, type = RegistryType.Plugins) {
332
+ // Loop through dependency packages and uninstall each one.
333
+ const pkgFile = packageLoadFile(filePath);
334
+ const manager = new ManagerLocal(type, this.config.config);
335
+ await manager.sync();
336
+ for (const slug in pkgFile[type]) {
337
+ await manager.uninstall(slug, pkgFile[type][slug]);
338
+ }
339
+ pkgFile.installed = true;
340
+ return pkgFile;
341
+ }
172
342
  }
@@ -5,12 +5,12 @@ export declare class Package {
5
5
  version: string;
6
6
  versions: Map<string, PackageVersion>;
7
7
  constructor(slug: string, versions?: PackageVersions);
8
- addVersion(version: string, details: PackageVersion): void;
9
- removeVersion(version: string): void;
8
+ addVersion(num: string, version: PackageVersion): void;
9
+ removeVersion(num: string): void;
10
10
  getReport(): {
11
11
  [k: string]: PackageReport;
12
12
  };
13
- getVersion(version: string): PackageVersion | undefined;
13
+ getVersion(num: string): PackageVersion | undefined;
14
14
  getVersionLatest(): PackageVersion | undefined;
15
15
  latestVersion(): string;
16
16
  toJSON(): {
@@ -14,33 +14,33 @@ export class Package {
14
14
  this.versions = versions ? new Map(Object.entries(versions)) : new Map();
15
15
  this.version = this.latestVersion();
16
16
  }
17
- addVersion(version, details) {
18
- if (this.versions.has(version))
17
+ addVersion(num, version) {
18
+ if (this.versions.has(num))
19
19
  return;
20
- const errors = packageErrors(details);
21
- const recs = packageRecommendations(details);
20
+ const errors = packageErrors(version);
21
+ const recs = packageRecommendations(version);
22
22
  const report = {
23
23
  ...(errors.length > 0 && { errors }),
24
24
  ...(recs.length > 0 && { recs }),
25
25
  };
26
26
  if (Object.keys(report).length > 0)
27
- this.reports.set(version, report);
27
+ this.reports.set(num, report);
28
28
  if (errors.length > 0)
29
29
  return;
30
- this.versions.set(version, details);
30
+ this.versions.set(num, version);
31
31
  this.version = this.latestVersion();
32
32
  }
33
- removeVersion(version) {
34
- if (!this.versions.has(version))
33
+ removeVersion(num) {
34
+ if (!this.versions.has(num))
35
35
  return;
36
- this.versions.delete(version);
36
+ this.versions.delete(num);
37
37
  this.version = this.latestVersion();
38
38
  }
39
39
  getReport() {
40
40
  return Object.fromEntries(this.reports);
41
41
  }
42
- getVersion(version) {
43
- return this.versions.get(version);
42
+ getVersion(num) {
43
+ return this.versions.get(num);
44
44
  }
45
45
  getVersionLatest() {
46
46
  return this.versions.get(this.latestVersion());
@@ -4,5 +4,5 @@ export declare class RegistryLocal extends Registry {
4
4
  protected managers: Record<string, ManagerLocal>;
5
5
  constructor(name: string, url: string, version: string);
6
6
  export(dir: string, ext?: string): boolean;
7
- scan(ext?: string): void;
7
+ scan(ext?: string, installable?: boolean): void;
8
8
  }
@@ -16,9 +16,9 @@ export class RegistryLocal extends Registry {
16
16
  saveFile(path.join(dir, `index.${ext}`), this.toJSON());
17
17
  return true;
18
18
  }
19
- scan(ext = 'json') {
19
+ scan(ext = 'json', installable = true) {
20
20
  for (const [, manager] of Object.entries(this.managers)) {
21
- manager.scan(ext);
21
+ manager.scan(ext, installable);
22
22
  }
23
23
  }
24
24
  }
@@ -28,6 +28,7 @@ export declare function fileDelete(filePath: string): boolean | void;
28
28
  export declare function fileExec(filePath: string): void;
29
29
  export declare function fileExists(filePath: string): boolean;
30
30
  export declare function fileHash(filePath: string, algorithm?: string): Promise<string>;
31
+ export declare function fileInstall(filePath: string): Buffer;
31
32
  export declare function fileMove(filePath: string, newPath: string): void | boolean;
32
33
  export declare function filesMove(dirSource: string, dirTarget: string, dirSub: string, formatDir: Record<string, string>): string[];
33
34
  export declare function fileOpen(filePath: string): Buffer;
@@ -14,7 +14,6 @@ import { ZodIssueCode } from 'zod';
14
14
  import { SystemType } from '../types/SystemType.js';
15
15
  import { fileURLToPath } from 'url';
16
16
  import sudoPrompt from '@vscode/sudo-prompt';
17
- import { log } from './utils.js';
18
17
  import { getSystem } from './utilsLocal.js';
19
18
  export async function archiveExtract(filePath, dirPath) {
20
19
  console.log('⎋', dirPath);
@@ -168,6 +167,36 @@ export async function fileHash(filePath, algorithm = 'sha256') {
168
167
  await stream.pipeline(input, hash);
169
168
  return hash.digest('hex');
170
169
  }
170
+ export function fileInstall(filePath) {
171
+ if (process.env.CI)
172
+ return Buffer.from('');
173
+ const ext = path.extname(filePath).toLowerCase();
174
+ let command = null;
175
+ switch (ext) {
176
+ case '.dmg':
177
+ command = `hdiutil attach -nobrowse "${filePath}" && sudo installer -pkg "$(find /Volumes -name '*.pkg' -maxdepth 2 | head -n 1)" -target / && hdiutil detach "$(find /Volumes -type d -maxdepth 1 -mindepth 1 | grep -v 'Macintosh HD')"`;
178
+ break;
179
+ case '.pkg':
180
+ command = `sudo installer -pkg "${filePath}" -target /`;
181
+ break;
182
+ case '.deb':
183
+ command = `sudo dpkg -i "${filePath}" || sudo apt-get install -f -y`;
184
+ break;
185
+ case '.rpm':
186
+ command = `sudo rpm -i --nodigest --nofiledigest --nosignature --force "${filePath}" || sudo dnf install -y "${filePath}" || sudo yum install -y "${filePath}"`;
187
+ break;
188
+ case '.exe':
189
+ command = `start /wait "" "${filePath}" /quiet /norestart`;
190
+ break;
191
+ case '.msi':
192
+ command = `msiexec /i "${filePath}" /quiet /norestart`;
193
+ break;
194
+ default:
195
+ throw new Error(`Unsupported file format: ${ext}`);
196
+ }
197
+ console.log('⎋', command);
198
+ return execSync(command, { stdio: 'inherit' });
199
+ }
171
200
  export function fileMove(filePath, newPath) {
172
201
  if (fileExists(filePath)) {
173
202
  console.log('-', filePath);
@@ -187,11 +216,11 @@ export function filesMove(dirSource, dirTarget, dirSub, formatDir) {
187
216
  const fileExt = path.extname(fileSource).slice(1).toLowerCase();
188
217
  const fileExtTarget = formatDir[fileExt];
189
218
  // If this is not a supported file format, then ignore.
190
- if (!fileExtTarget)
191
- return;
219
+ if (fileExtTarget === undefined)
220
+ return console.error(`${fileSource} - ${fileExt} not mapped to a installation folder, skipping.`);
192
221
  const fileTarget = path.join(dirTarget, fileExtTarget, dirSub, path.basename(fileSource));
193
222
  if (fileExists(fileTarget))
194
- return;
223
+ return console.error(`${fileSource} - ${fileTarget} already exists, skipping.`);
195
224
  dirCreate(path.dirname(fileTarget));
196
225
  fileMove(fileSource, fileTarget);
197
226
  filesMoved.push(fileTarget);
@@ -281,13 +310,13 @@ export function runCliAsAdmin(args) {
281
310
  return new Promise((resolve, reject) => {
282
311
  const filename = fileURLToPath(import.meta.url).replace('src/', 'build/');
283
312
  const dirPathClean = dirname(filename).replace('app.asar', 'app.asar.unpacked');
284
- log(`node "${dirPathClean}${path.sep}helpers${path.sep}admin.js" ${args}`);
285
- sudoPrompt.exec(`node "${dirPathClean}${path.sep}helpers${path.sep}admin.js" ${args}`, { name: 'Open Audio Stack' }, (error, stdout, stderr) => {
313
+ const script = path.join(dirPathClean, 'admin.js');
314
+ sudoPrompt.exec(`node "${script}" ${args}`, { name: 'Open Audio Stack' }, (error, stdout, stderr) => {
286
315
  if (stdout) {
287
- log('runCliAsAdmin', stdout);
316
+ console.log('runCliAsAdmin', stdout);
288
317
  }
289
318
  if (stderr) {
290
- log('runCliAsAdmin', stderr);
319
+ console.log('runCliAsAdmin', stderr);
291
320
  }
292
321
  if (error) {
293
322
  reject(error);
@@ -172,10 +172,10 @@ export declare const PackageVersionValidator: z.ZodObject<{
172
172
  }>;
173
173
  url: z.ZodString;
174
174
  }, "strip", z.ZodTypeAny, {
175
+ date: string;
175
176
  audio: string;
176
177
  author: string;
177
178
  changes: string;
178
- date: string;
179
179
  description: string;
180
180
  files: {
181
181
  type: FileType;
@@ -197,10 +197,10 @@ export declare const PackageVersionValidator: z.ZodObject<{
197
197
  name: string;
198
198
  tags: string[];
199
199
  }, {
200
+ date: string;
200
201
  audio: string;
201
202
  author: string;
202
203
  changes: string;
203
- date: string;
204
204
  description: string;
205
205
  files: {
206
206
  type: FileType;
@@ -8,6 +8,7 @@ import { PluginType } from '../types/PluginType.js';
8
8
  import { PresetType } from '../types/PresetType.js';
9
9
  import { ProjectType } from '../types/ProjectType.js';
10
10
  import { SystemType } from '../types/SystemType.js';
11
+ import { pathGetExt } from './utils.js';
11
12
  export function packageCompatibleFiles(pkg, arch, sys, excludedFormats) {
12
13
  return pkg.files.filter((file) => {
13
14
  const archMatches = file.architectures.filter(architecture => {
@@ -16,7 +17,7 @@ export function packageCompatibleFiles(pkg, arch, sys, excludedFormats) {
16
17
  const sysMatches = file.systems.filter(system => {
17
18
  return sys.includes(system.type);
18
19
  });
19
- const formatAllowed = excludedFormats && excludedFormats.includes(file.format) ? false : true;
20
+ const formatAllowed = excludedFormats && excludedFormats.includes(pathGetExt(file.url)) ? false : true;
20
21
  return archMatches.length && sysMatches.length && formatAllowed;
21
22
  });
22
23
  }
@@ -88,59 +89,85 @@ export function packageRecommendations(pkgVersion) {
88
89
  rec: 'should use the flac format',
89
90
  });
90
91
  }
91
- const supportedArchitectures = {};
92
- const supportedSystems = {};
93
- const supportedFileFormats = {};
94
- pkgVersion.files.forEach(file => {
95
- file.architectures.forEach(architecture => {
96
- supportedArchitectures[architecture] = true;
92
+ // Files
93
+ if (pkgVersion.files) {
94
+ const supportedArchitectures = {};
95
+ const supportedSystems = {};
96
+ const supportedFileFormats = {};
97
+ pkgVersion.files.forEach(file => {
98
+ file.architectures.forEach(architecture => {
99
+ supportedArchitectures[architecture] = true;
100
+ });
101
+ file.systems.forEach(system => {
102
+ supportedSystems[system.type] = true;
103
+ });
104
+ const ext = pathGetExt(file.url);
105
+ supportedFileFormats[ext] = true;
106
+ packageRecommendationsUrl(file, recs, 'url');
107
+ // Formats which do not support headless installation.
108
+ if (ext === FileFormat.AppImage)
109
+ recs.push({ field: 'url', rec: 'requires manual installation steps, consider .deb and .rpm instead' });
110
+ if (ext === FileFormat.AppleDiskImage)
111
+ recs.push({ field: 'url', rec: 'requires mounting step, consider .pkg instead' });
112
+ if (!Object.values(FileFormat).includes(ext))
113
+ recs.push({ field: 'url', rec: 'not a supported format' });
114
+ // Validate format is a supported installer format
115
+ const installerFormats = [
116
+ FileFormat.AppleDiskImage,
117
+ FileFormat.ApplePackage,
118
+ FileFormat.DebianPackage,
119
+ FileFormat.ExecutableInstaller,
120
+ FileFormat.RedHatPackage,
121
+ FileFormat.WindowsInstaller,
122
+ ];
123
+ if (file.type === FileType.Installer && !installerFormats.includes(ext))
124
+ recs.push({ field: 'type', rec: 'should match url field' });
97
125
  });
98
- file.systems.forEach(system => {
99
- supportedSystems[system.type] = true;
126
+ // Architectures
127
+ if (!supportedArchitectures.arm64)
128
+ recs.push({ field: 'architectures', rec: 'should support arm64' });
129
+ if (!supportedArchitectures.x64)
130
+ recs.push({ field: 'architectures', rec: 'should support x64' });
131
+ // Systems
132
+ if (!supportedSystems.linux)
133
+ recs.push({ field: 'systems', rec: 'should support Linux' });
134
+ if (!supportedSystems.mac)
135
+ recs.push({ field: 'systems', rec: 'should support Mac' });
136
+ if (!supportedSystems.win)
137
+ recs.push({ field: 'systems', rec: 'should support Windows' });
138
+ }
139
+ else {
140
+ recs.push({
141
+ field: 'files',
142
+ rec: 'is missing',
100
143
  });
101
- supportedFileFormats[file.format] = true;
102
- packageRecommendationsUrl(file, recs, 'url');
103
- // Formats which do not support headless installation.
104
- if (file.format === FileFormat.AppImage)
105
- recs.push({ field: 'format', rec: 'requires manual installation steps, consider .deb and .rpm instead' });
106
- if (file.format === FileFormat.AppleDiskImage)
107
- recs.push({ field: 'format', rec: 'requires mounting step, consider .pkg instead' });
108
- if (!file.url.includes(file.format))
109
- recs.push({ field: 'format', rec: 'should match url field' });
110
- // Validate format is a supported installer format
111
- const installerFormats = [
112
- FileFormat.AppleDiskImage,
113
- FileFormat.ApplePackage,
114
- FileFormat.DebianPackage,
115
- FileFormat.ExecutableInstaller,
116
- FileFormat.RedHatPackage,
117
- FileFormat.WindowsInstaller,
118
- ];
119
- if (file.type === FileType.Installer && !installerFormats.includes(file.format))
120
- recs.push({ field: 'type', rec: 'should match format field' });
121
- });
122
- // Architectures
123
- if (!supportedArchitectures.arm64)
124
- recs.push({ field: 'architectures', rec: 'should support arm64' });
125
- if (!supportedArchitectures.x64)
126
- recs.push({ field: 'architectures', rec: 'should support x64' });
127
- // Systems
128
- if (!supportedSystems.linux)
129
- recs.push({ field: 'systems', rec: 'should support Linux' });
130
- if (!supportedSystems.mac)
131
- recs.push({ field: 'systems', rec: 'should support Mac' });
132
- if (!supportedSystems.win)
133
- recs.push({ field: 'systems', rec: 'should support Windows' });
144
+ }
134
145
  // Tags
135
- const pluginTags = pkgVersion.tags.map(tag => tag.trim().toLowerCase());
136
- if (pluginTags.length < 2)
137
- recs.push({ field: 'tags', rec: 'should have more items' });
146
+ if (pkgVersion.tags) {
147
+ const pluginTags = pkgVersion.tags.map(tag => tag.trim().toLowerCase());
148
+ if (pluginTags.length < 2)
149
+ recs.push({ field: 'tags', rec: 'should have more items' });
150
+ }
151
+ else {
152
+ recs.push({
153
+ field: 'tags',
154
+ rec: 'is missing',
155
+ });
156
+ }
138
157
  // Licence
139
- if (!Object.values(License).includes(pkgVersion.license)) {
140
- recs.push({ field: 'license', rec: 'should be from the supported list' });
158
+ if (pkgVersion.license) {
159
+ if (!Object.values(License).includes(pkgVersion.license)) {
160
+ recs.push({ field: 'license', rec: 'should be from the supported list' });
161
+ }
162
+ else if (pkgVersion.license === License.Other) {
163
+ recs.push({ field: 'license', rec: 'should be more specific' });
164
+ }
141
165
  }
142
- else if (pkgVersion.license === License.Other) {
143
- recs.push({ field: 'license', rec: 'should be more specific' });
166
+ else {
167
+ recs.push({
168
+ field: 'license',
169
+ rec: 'is missing',
170
+ });
144
171
  }
145
172
  return recs;
146
173
  }
@@ -0,0 +1,3 @@
1
+ import { PackageVersion } from '../index-browser.js';
2
+ export declare function packageLoadFile(filePath?: string): PackageVersion;
3
+ export declare function packageSaveFile(pkgFile: PackageVersion, filePath?: string): PackageVersion;
@@ -0,0 +1,24 @@
1
+ import path from 'path';
2
+ import { fileCreateJson, fileReadJson } from './file.js';
3
+ import { Package } from '../classes/Package.js';
4
+ import { pathGetSlug, pathGetVersion } from './utils.js';
5
+ export function packageLoadFile(filePath) {
6
+ // Read the file path and parse as json.
7
+ if (!filePath)
8
+ filePath = path.join('.', 'index.json');
9
+ const pkgFile = fileReadJson(filePath);
10
+ if (!pkgFile)
11
+ console.error(filePath, `not a valid json file`);
12
+ // Validate package json file structure, fields and values.
13
+ const pkg = new Package(pathGetSlug(filePath));
14
+ pkg.addVersion(pathGetVersion(filePath), pkgFile);
15
+ console.log(JSON.stringify(pkg.getReport()));
16
+ return pkgFile;
17
+ }
18
+ export function packageSaveFile(pkgFile, filePath) {
19
+ // Read the file path and parse as json.
20
+ if (!filePath)
21
+ filePath = path.join('.', 'index.json');
22
+ fileCreateJson(filePath, pkgFile);
23
+ return pkgFile;
24
+ }
@@ -12,4 +12,6 @@ export declare function pathGetExt(path: string, sep?: string): string;
12
12
  export declare function pathGetFilename(path: string, sep?: string): string;
13
13
  export declare function pathGetSlug(path: string, sep?: string): string;
14
14
  export declare function pathGetVersion(path: string, sep?: string): string;
15
+ export declare function toSlug(val: string): string;
15
16
  export declare function isValidSlug(slug: string): boolean;
17
+ export declare function isValidVersion(version: string): boolean;
@@ -1,5 +1,8 @@
1
1
  import chalk from 'chalk';
2
+ import * as semver from 'semver';
3
+ import slugify from 'slugify';
2
4
  let LOGGING_ENABLED = false;
5
+ const URLSAFE_REGEX = /[^\w\s$*_+~.()'"!\-:@/]+/g;
3
6
  export function inputGetParts(input) {
4
7
  return input.split('@');
5
8
  }
@@ -62,6 +65,10 @@ export function pathGetVersion(path, sep = '/') {
62
65
  const parts = path.split(sep);
63
66
  return parts[parts.length - 2];
64
67
  }
68
+ export function toSlug(val) {
69
+ // @ts-expect-error slugify library issue with ESM modules
70
+ return slugify(val, { lower: true, remove: URLSAFE_REGEX });
71
+ }
65
72
  export function isValidSlug(slug) {
66
73
  let valid = true;
67
74
  // Must have exactly one slash.
@@ -70,5 +77,11 @@ export function isValidSlug(slug) {
70
77
  // Must be lowercase.
71
78
  if (slug !== slug.toLowerCase())
72
79
  valid = false;
80
+ // Must pass slugify conversion
81
+ if (slug !== toSlug(slug))
82
+ valid = false;
73
83
  return valid;
74
84
  }
85
+ export function isValidVersion(version) {
86
+ return semver.valid(version) !== null;
87
+ }
@@ -17,6 +17,7 @@ export * from './helpers/config.js';
17
17
  // export * from './helpers/configLocal.js';
18
18
  // export * from './helpers/file.js';
19
19
  export * from './helpers/package.js';
20
+ // export * from './helpers/packageLocal.js';
20
21
  export * from './helpers/registry.js';
21
22
  export * from './helpers/utils.js';
22
23
  // export * from './helpers/utilsLocal.js';
package/build/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './helpers/config.js';
10
10
  export * from './helpers/configLocal.js';
11
11
  export * from './helpers/file.js';
12
12
  export * from './helpers/package.js';
13
+ export * from './helpers/packageLocal.js';
13
14
  export * from './helpers/registry.js';
14
15
  export * from './helpers/utils.js';
15
16
  export * from './helpers/utilsLocal.js';
package/build/index.js CHANGED
@@ -17,6 +17,7 @@ export * from './helpers/config.js';
17
17
  export * from './helpers/configLocal.js';
18
18
  export * from './helpers/file.js';
19
19
  export * from './helpers/package.js';
20
+ export * from './helpers/packageLocal.js';
20
21
  export * from './helpers/registry.js';
21
22
  export * from './helpers/utils.js';
22
23
  export * from './helpers/utilsLocal.js';
@@ -1,10 +1,8 @@
1
1
  import { Architecture } from './Architecture.js';
2
- import { FileFormat } from './FileFormat.js';
3
2
  import { FileType } from './FileType.js';
4
3
  import { System } from './System.js';
5
4
  export interface FileInterface {
6
5
  architectures: Architecture[];
7
- format: FileFormat;
8
6
  sha256: string;
9
7
  size: number;
10
8
  systems: System[];
@@ -2,9 +2,10 @@ import { FileInterface } from './File.js';
2
2
  import { PackageBase } from './Package.js';
3
3
  import { PresetFormat } from './PresetFormat.js';
4
4
  import { PresetType } from './PresetType.js';
5
+ import { RegistryType } from './Registry.js';
5
6
  export interface PresetInterface extends PackageBase {
6
7
  files: PresetFile[];
7
- plugins: PresetPlugins;
8
+ [RegistryType.Plugins]: PresetPlugins;
8
9
  type: PresetType;
9
10
  }
10
11
  export interface PresetFile extends FileInterface {
@@ -1 +1 @@
1
- export {};
1
+ import { RegistryType } from './Registry.js';
@@ -2,9 +2,11 @@ import { FileInterface } from './File.js';
2
2
  import { PackageBase } from './Package.js';
3
3
  import { ProjectFormat } from './ProjectFormat.js';
4
4
  import { ProjectType } from './ProjectType.js';
5
+ import { RegistryType } from './Registry.js';
5
6
  export interface ProjectInterface extends PackageBase {
6
7
  files: ProjectFile[];
7
- plugins: ProjectPlugins;
8
+ open: string;
9
+ [RegistryType.Plugins]: ProjectPlugins;
8
10
  type: ProjectType;
9
11
  }
10
12
  export interface ProjectFile extends FileInterface {
@@ -1 +1 @@
1
- export {};
1
+ import { RegistryType } from './Registry.js';
@@ -13,19 +13,20 @@ export var ProjectFormat;
13
13
  ProjectFormat["Reaper"] = "rpp";
14
14
  ProjectFormat["Sonar"] = "cwp";
15
15
  })(ProjectFormat || (ProjectFormat = {}));
16
+ // TODO don't organize projects by type, as this requires knwoing the type in advance for other operations.
16
17
  export const projectFormatDir = {
17
- [ProjectFormat.AbletonLive]: 'Ableton',
18
- [ProjectFormat.Bitwig]: 'Bitwig',
19
- [ProjectFormat.Cubase]: 'Cubase',
20
- [ProjectFormat.DAWproject]: 'DAWproject',
21
- [ProjectFormat.FLStudio]: 'FLStudio',
22
- [ProjectFormat.Garageband]: 'Garageband',
23
- [ProjectFormat.Lmms]: 'Lmms',
24
- [ProjectFormat.Logic]: 'Logic',
25
- [ProjectFormat.Musescore]: 'Musescore',
26
- [ProjectFormat.ProTools]: 'ProTools',
27
- [ProjectFormat.Reaper]: 'Reaper',
28
- [ProjectFormat.Sonar]: 'Sonar',
18
+ [ProjectFormat.AbletonLive]: '',
19
+ [ProjectFormat.Bitwig]: '',
20
+ [ProjectFormat.Cubase]: '',
21
+ [ProjectFormat.DAWproject]: '',
22
+ [ProjectFormat.FLStudio]: '',
23
+ [ProjectFormat.Garageband]: '',
24
+ [ProjectFormat.Lmms]: '',
25
+ [ProjectFormat.Logic]: '',
26
+ [ProjectFormat.Musescore]: '',
27
+ [ProjectFormat.ProTools]: '',
28
+ [ProjectFormat.Reaper]: '',
29
+ [ProjectFormat.Sonar]: '',
29
30
  };
30
31
  export const projectFormats = [
31
32
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-audio-stack/core",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Open-source audio plugin management software",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",
@@ -14,6 +14,7 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "tsc -p tsconfig.json",
17
+ "check": "npm run format && npm run lint && npm run build",
17
18
  "dev": "tsx ./src/index.ts",
18
19
  "format": "prettier . --write",
19
20
  "lint": "eslint .",
@@ -58,8 +59,10 @@
58
59
  "chalk": "^5.3.0",
59
60
  "fs-extra": "^11.2.0",
60
61
  "glob": "^11.0.0",
62
+ "inquirer": "^12.4.1",
61
63
  "js-yaml": "^4.1.0",
62
64
  "semver": "^7.6.3",
65
+ "slugify": "^1.6.6",
63
66
  "tar": "^7.4.3",
64
67
  "zod": "^3.23.8"
65
68
  },