@open-audio-stack/core 0.1.45 → 0.1.46

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.
@@ -1,7 +1,7 @@
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, dirMove, dirRead, fileCreate, fileCreateJson, fileCreateYaml, fileExists, fileHash, fileInstall, fileOpen, fileReadJson, fileReadYaml, filesMove, isAdmin, runCliAsAdmin, } from '../helpers/file.js';
4
+ import { archiveExtract, dirCreate, dirDelete, dirEmpty, dirIs, dirMove, dirRead, fileCreate, fileCreateJson, fileCreateYaml, fileExists, fileHash, fileInstall, fileOpen, fileReadJson, fileReadYaml, filesMove, isAdmin, runCliAsAdmin, } from '../helpers/file.js';
5
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';
@@ -260,12 +260,43 @@ export class ManagerLocal extends Manager {
260
260
  fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion);
261
261
  }
262
262
  else {
263
- // Move only supported file extensions into their respective installation directories.
264
- const filesMoved = filesMove(dirSource, this.typeDir, dirSub, formatDir);
265
- filesMoved.forEach((fileMoved) => {
266
- const fileJson = path.join(path.dirname(fileMoved), 'index.json');
267
- fileCreateJson(fileJson, pkgVersion);
263
+ // Check if archive contains installer files (pkg, dmg) that should be run
264
+ const allFiles = dirRead(`${dirSource}/**/*`).filter(f => !dirIs(f));
265
+ const installerFiles = allFiles.filter(f => {
266
+ const ext = path.extname(f).toLowerCase();
267
+ return ext === '.pkg' || ext === '.dmg';
268
268
  });
269
+ if (installerFiles.length > 0) {
270
+ // Run installer files found in archive
271
+ for (const installerFile of installerFiles) {
272
+ if (isTests())
273
+ fileOpen(installerFile);
274
+ else
275
+ fileInstall(installerFile);
276
+ }
277
+ // Create directory and save package info for installer
278
+ const dirTarget = path.join(this.typeDir, 'Installers', dirSub);
279
+ dirCreate(dirTarget);
280
+ fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion);
281
+ }
282
+ else if (this.type === RegistryType.Plugins) {
283
+ // For plugins, move files into type-specific subdirectories
284
+ const filesMoved = filesMove(dirSource, this.typeDir, dirSub, formatDir);
285
+ if (filesMoved.length === 0) {
286
+ throw new Error(`No compatible files found to install for ${slug}`);
287
+ }
288
+ filesMoved.forEach((fileMoved) => {
289
+ const fileJson = path.join(path.dirname(fileMoved), 'index.json');
290
+ fileCreateJson(fileJson, pkgVersion);
291
+ });
292
+ }
293
+ else {
294
+ // For apps/projects/presets, move entire directory without type subdirectories
295
+ const dirTarget = path.join(this.typeDir, dirSub);
296
+ dirCreate(dirTarget);
297
+ dirMove(dirSource, dirTarget);
298
+ fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion);
299
+ }
269
300
  }
270
301
  }
271
302
  }
@@ -282,9 +313,9 @@ export class ManagerLocal extends Manager {
282
313
  return this.listPackages();
283
314
  }
284
315
  // Loop through all packages and install each one.
285
- for (const [slug, pkg] of this.packages) {
316
+ for (const pkg of this.listPackages()) {
286
317
  const versionNum = pkg.latestVersion();
287
- await this.install(slug, versionNum);
318
+ await this.install(pkg.slug, versionNum);
288
319
  }
289
320
  return this.listPackages();
290
321
  }
@@ -295,15 +326,17 @@ export class ManagerLocal extends Manager {
295
326
  manager.scan();
296
327
  const pkg = manager.getPackage(slug);
297
328
  if (!pkg)
298
- return this.log(`Package ${slug} not found in registry`);
329
+ throw new Error(`Package ${slug} not found in registry`);
299
330
  const versionNum = version || pkg.latestVersion();
300
331
  const pkgVersion = pkg?.getVersion(versionNum);
301
332
  if (!pkgVersion)
302
- return this.log(`Package ${slug} version ${versionNum} not found in registry`);
333
+ throw new Error(`Package ${slug} version ${versionNum} not found in registry`);
303
334
  // Get local package file.
304
335
  const pkgFile = packageLoadFile(filePath);
305
336
  if (pkgFile[type] && pkgFile[type][slug] && pkgFile[type][slug] === versionNum) {
306
- return this.log(`Package ${slug} version ${versionNum} is already a dependency`);
337
+ this.log(`Package ${slug} version ${versionNum} is already a dependency`);
338
+ pkgFile.installed = true;
339
+ return pkgFile;
307
340
  }
308
341
  // Install dependency.
309
342
  await manager.install(slug, version);
@@ -434,9 +467,9 @@ export class ManagerLocal extends Manager {
434
467
  // Get local package file.
435
468
  const pkgFile = packageLoadFile(filePath);
436
469
  if (!pkgFile[type])
437
- return this.log(`Package ${type} is missing`);
470
+ throw new Error(`Package ${type} is missing`);
438
471
  if (!pkgFile[type][slug])
439
- return this.log(`Package ${type} ${slug} is not a dependency`);
472
+ throw new Error(`Package ${type} ${slug} is not a dependency`);
440
473
  // Uninstall dependency.
441
474
  const manager = new ManagerLocal(type, this.config.config);
442
475
  await manager.sync();
@@ -28,7 +28,7 @@ export class Package extends Base {
28
28
  if (Object.keys(report).length > 0)
29
29
  this.reports.set(num, report);
30
30
  if (errors.length > 0)
31
- return this.log(`Package ${version.name} version ${num} errors`, errors);
31
+ throw new Error(`Package ${version.name} version ${num} has validation errors: ${JSON.stringify(errors)}`);
32
32
  version.verified = packageIsVerified(this.slug, version);
33
33
  this.versions.set(num, version);
34
34
  this.version = this.latestVersion();
@@ -35,14 +35,14 @@ export function adminArguments() {
35
35
  return args;
36
36
  }
37
37
  export async function adminInit() {
38
- const args = adminArguments();
39
- const manager = new ManagerLocal(args.type, { appDir: args.appDir });
40
- if (args.log)
41
- manager.logEnable();
42
- manager.log('adminInit', args);
43
- await manager.sync();
44
- manager.scan();
45
38
  try {
39
+ const args = adminArguments();
40
+ const manager = new ManagerLocal(args.type, { appDir: args.appDir });
41
+ if (args.log)
42
+ manager.logEnable();
43
+ manager.log('adminInit', args);
44
+ await manager.sync();
45
+ manager.scan();
46
46
  if (args.operation === 'install') {
47
47
  await manager.install(args.id, args.version);
48
48
  }
@@ -61,7 +61,7 @@ export async function adminInit() {
61
61
  const message = err && err.message ? err.message : String(err);
62
62
  const errorResult = { status: 'error', code: err && err.code ? err.code : 1, message };
63
63
  process.stdout.write('\n');
64
- console.log(JSON.stringify(errorResult));
64
+ console.error(JSON.stringify(errorResult));
65
65
  process.exit(typeof errorResult.code === 'number' ? errorResult.code : 1);
66
66
  }
67
67
  }
@@ -16,9 +16,14 @@ import { fileURLToPath } from 'url';
16
16
  import sudoPrompt from '@vscode/sudo-prompt';
17
17
  import { getSystem } from './utilsLocal.js';
18
18
  import { log } from './utils.js';
19
+ import mime from 'mime-types';
19
20
  export async function archiveExtract(filePath, dirPath) {
20
21
  log('⎋', dirPath);
22
+ const fileName = path.basename(filePath).toLowerCase();
21
23
  const ext = path.extname(filePath).trim().toLowerCase();
24
+ const tarExtensions = ['.tar', '.gz', '.tgz', '.xz', '.bz2', '.tbz2'];
25
+ const tarCompoundExtensions = ['.tar.gz', '.tar.xz', '.tar.bz2'];
26
+ const isTarFile = tarExtensions.includes(ext) || tarCompoundExtensions.some(compoundExt => fileName.endsWith(compoundExt));
22
27
  if (ext === '.zip') {
23
28
  const zip = new AdmZip(filePath);
24
29
  try {
@@ -44,7 +49,7 @@ export async function archiveExtract(filePath, dirPath) {
44
49
  }
45
50
  }
46
51
  }
47
- else if (ext === '.tar' || ext === '.gz' || ext === '.tgz') {
52
+ else if (isTarFile) {
48
53
  return await tar.extract({
49
54
  file: filePath,
50
55
  cwd: dirPath,
@@ -252,15 +257,32 @@ export function fileMove(filePath, newPath) {
252
257
  export function filesMove(dirSource, dirTarget, dirSub, formatDir) {
253
258
  const filesAndFolders = dirRead(`${dirSource}/**/*`);
254
259
  log('filesAndFolders', filesAndFolders);
260
+ // First pass: identify bundle directories (app, clap, vst3, lv2, etc.)
261
+ const bundleDirs = new Set();
262
+ filesAndFolders.forEach(f => {
263
+ if (dirIs(f)) {
264
+ // Check if this is a macOS application bundle or plugin bundle
265
+ if (fileExists(path.join(f, 'Contents', 'Info.plist'))) {
266
+ bundleDirs.add(f);
267
+ }
268
+ // Check if this is an LV2 plugin folder
269
+ if (fileExists(path.join(f, 'manifest.ttl'))) {
270
+ bundleDirs.add(f);
271
+ }
272
+ }
273
+ });
255
274
  const files = filesAndFolders.filter(f => {
256
- // Include files.
275
+ // Exclude files/folders that are inside bundle directories
276
+ for (const bundleDir of bundleDirs) {
277
+ if (f.startsWith(bundleDir + path.sep)) {
278
+ return false; // This path is inside a bundle, exclude it
279
+ }
280
+ }
281
+ // Include regular files (not directories).
257
282
  if (!dirIs(f))
258
283
  return true;
259
- // Include macOS application bundles (directory of files presented as a single file).
260
- if (fileExists(path.join(f, 'Contents', 'Info.plist')))
261
- return true;
262
- // Include LV2 plugin folders.
263
- if (fileExists(path.join(f, 'manifest.ttl')))
284
+ // Include bundle directories themselves (already identified above).
285
+ if (bundleDirs.has(f))
264
286
  return true;
265
287
  // Otherwise ignore.
266
288
  return false;
@@ -270,7 +292,14 @@ export function filesMove(dirSource, dirTarget, dirSub, formatDir) {
270
292
  // For each file, move to correct folder based on type
271
293
  files.forEach((fileSource) => {
272
294
  const fileExt = path.extname(fileSource).slice(1).toLowerCase();
273
- const fileExtTarget = formatDir[fileExt];
295
+ let fileExtTarget = formatDir[fileExt];
296
+ // Use mime-type detection as fallback for unmapped extensions
297
+ if (!fileExtTarget) {
298
+ const mimeType = mime.lookup(fileSource) || '';
299
+ if (!mimeType || mimeType.startsWith('application/')) {
300
+ fileExtTarget = 'App';
301
+ }
302
+ }
274
303
  // If this is not a supported file format, then ignore.
275
304
  if (fileExtTarget === undefined)
276
305
  return log(`${fileSource} - ${fileExt || 'no extension'} not mapped to a installation folder, skipping.`);
@@ -392,14 +421,6 @@ export function runCliAsAdmin(args) {
392
421
  log(`Running as admin: node "${script}" ${args}`);
393
422
  const cmd = `node "${script}" ${args}`;
394
423
  sudoPrompt.exec(cmd, { name: 'Open Audio Stack' }, (error, stdout, stderr) => {
395
- // Prefer explicit error from sudo-prompt callback
396
- if (error) {
397
- const stderrStr = stderr ? (typeof stderr === 'string' ? stderr : stderr.toString()) : '';
398
- const msg = `runCliAsAdmin: admin command failed: ${error && error.message ? error.message : String(error)}${stderrStr ? `\nstderr: ${stderrStr}` : ''}`;
399
- const err = new Error(msg);
400
- err.code = error && error.code ? error.code : undefined;
401
- return reject(err);
402
- }
403
424
  // Convert stdout/stderr buffers to strings for inspection
404
425
  const stdoutStr = stdout ? (typeof stdout === 'string' ? stdout : stdout.toString()) : '';
405
426
  const stderrStr = stderr ? (typeof stderr === 'string' ? stderr : stderr.toString()) : '';
@@ -421,6 +442,7 @@ export function runCliAsAdmin(args) {
421
442
  // This line is not JSON, continue searching
422
443
  }
423
444
  }
445
+ // If we found JSON output from admin script, prioritize it over sudoPrompt error
424
446
  if (jsonPayload) {
425
447
  if (jsonPayload && (jsonPayload.status === 'ok' || jsonPayload.code === 0)) {
426
448
  return resolve();
@@ -428,6 +450,13 @@ export function runCliAsAdmin(args) {
428
450
  const errMsg = jsonPayload && jsonPayload.message ? jsonPayload.message : JSON.stringify(jsonPayload);
429
451
  return reject(new Error(`runCliAsAdmin: admin command reported error: ${errMsg}`));
430
452
  }
453
+ // If no JSON found, check for sudoPrompt error
454
+ if (error) {
455
+ const msg = `runCliAsAdmin: admin command failed: ${error && error.message ? error.message : String(error)}${stderrStr ? `\nstderr: ${stderrStr}` : ''}`;
456
+ const err = new Error(msg);
457
+ err.code = error && error.code ? error.code : undefined;
458
+ return reject(err);
459
+ }
431
460
  return reject(new Error(`runCliAsAdmin: admin command did not report completion. stdout: ${stdoutStr} stderr: ${stderrStr}`));
432
461
  });
433
462
  });
@@ -19,9 +19,7 @@ export declare enum PluginFormat {
19
19
  export type PluginFormatDir = {
20
20
  [format in PluginFormat]: string;
21
21
  };
22
- export declare const pluginFormatDir: PluginFormatDir & {
23
- '': string;
24
- };
22
+ export declare const pluginFormatDir: PluginFormatDir;
25
23
  export interface PluginFormatOption {
26
24
  description: string;
27
25
  value: PluginFormat;
@@ -34,7 +34,6 @@ export const pluginFormatDir = {
34
34
  [PluginFormat.VSTMac]: 'VST',
35
35
  [PluginFormat.VSTWin]: 'VST',
36
36
  [PluginFormat.WinStandalone]: 'Exe',
37
- '': 'App',
38
37
  };
39
38
  export const pluginFormats = [
40
39
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-audio-stack/core",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "Open-source audio plugin management software",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",
@@ -42,6 +42,7 @@
42
42
  "@types/adm-zip": "^0.5.5",
43
43
  "@types/fs-extra": "^11.0.4",
44
44
  "@types/js-yaml": "^4.0.9",
45
+ "@types/mime-types": "^3.0.1",
45
46
  "@types/node": "^22.7.8",
46
47
  "@types/semver": "^7.5.8",
47
48
  "@vitest/coverage-v8": "^3.0.5",
@@ -62,6 +63,7 @@
62
63
  "glob": "^11.0.0",
63
64
  "inquirer": "^12.4.1",
64
65
  "js-yaml": "^4.1.0",
66
+ "mime-types": "^3.0.2",
65
67
  "semver": "^7.6.3",
66
68
  "slugify": "^1.6.6",
67
69
  "tar": "^7.4.3",