@open-audio-stack/core 0.1.45 → 0.1.47

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, fileExec, 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,85 @@ 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
+ // Ensure executable permissions for likely executables inside moved app/project/preset
300
+ try {
301
+ const movedFiles = dirRead(path.join(dirTarget, '**', '*')).filter(f => !dirIs(f));
302
+ movedFiles.forEach((movedFile) => {
303
+ const ext = path.extname(movedFile).slice(1).toLowerCase();
304
+ if (['', 'elf', 'exe'].includes(ext)) {
305
+ try {
306
+ fileExec(movedFile);
307
+ }
308
+ catch (err) {
309
+ this.log(`Failed to set exec on ${movedFile}:`, err);
310
+ }
311
+ }
312
+ });
313
+ }
314
+ catch (err) {
315
+ this.log('Error while setting executable permissions:', err);
316
+ }
317
+ // Also handle macOS .app bundles: set exec on binaries in Contents/MacOS
318
+ try {
319
+ const appDirs = dirRead(path.join(dirTarget, '**', '*.app')).filter(d => dirIs(d));
320
+ appDirs.forEach((appDir) => {
321
+ try {
322
+ const macosBinPattern = path.join(appDir, 'Contents', 'MacOS', '**', '*');
323
+ const macosFiles = dirRead(macosBinPattern).filter(f => !dirIs(f));
324
+ macosFiles.forEach((binFile) => {
325
+ try {
326
+ fileExec(binFile);
327
+ }
328
+ catch (err) {
329
+ this.log(`Failed to set exec on app binary ${binFile}:`, err);
330
+ }
331
+ });
332
+ }
333
+ catch (err) {
334
+ this.log(`Error scanning .app contents for ${appDir}:`, err);
335
+ }
336
+ });
337
+ }
338
+ catch (err) {
339
+ this.log(err);
340
+ }
341
+ }
269
342
  }
270
343
  }
271
344
  }
@@ -282,9 +355,9 @@ export class ManagerLocal extends Manager {
282
355
  return this.listPackages();
283
356
  }
284
357
  // Loop through all packages and install each one.
285
- for (const [slug, pkg] of this.packages) {
358
+ for (const pkg of this.listPackages()) {
286
359
  const versionNum = pkg.latestVersion();
287
- await this.install(slug, versionNum);
360
+ await this.install(pkg.slug, versionNum);
288
361
  }
289
362
  return this.listPackages();
290
363
  }
@@ -295,15 +368,17 @@ export class ManagerLocal extends Manager {
295
368
  manager.scan();
296
369
  const pkg = manager.getPackage(slug);
297
370
  if (!pkg)
298
- return this.log(`Package ${slug} not found in registry`);
371
+ throw new Error(`Package ${slug} not found in registry`);
299
372
  const versionNum = version || pkg.latestVersion();
300
373
  const pkgVersion = pkg?.getVersion(versionNum);
301
374
  if (!pkgVersion)
302
- return this.log(`Package ${slug} version ${versionNum} not found in registry`);
375
+ throw new Error(`Package ${slug} version ${versionNum} not found in registry`);
303
376
  // Get local package file.
304
377
  const pkgFile = packageLoadFile(filePath);
305
378
  if (pkgFile[type] && pkgFile[type][slug] && pkgFile[type][slug] === versionNum) {
306
- return this.log(`Package ${slug} version ${versionNum} is already a dependency`);
379
+ this.log(`Package ${slug} version ${versionNum} is already a dependency`);
380
+ pkgFile.installed = true;
381
+ return pkgFile;
307
382
  }
308
383
  // Install dependency.
309
384
  await manager.install(slug, version);
@@ -352,14 +427,16 @@ export class ManagerLocal extends Manager {
352
427
  try {
353
428
  const openPath = openableFile.open;
354
429
  const fileExt = path.extname(openPath).slice(1).toLowerCase();
355
- let formatDir = pluginFormatDir[fileExt] || 'Plugin';
356
- if (this.type === RegistryType.Apps)
357
- formatDir = pluginFormatDir[fileExt] || 'App';
358
- else if (this.type === RegistryType.Presets)
359
- formatDir = presetFormatDir[fileExt] || 'Preset';
360
- else if (this.type === RegistryType.Projects)
361
- formatDir = projectFormatDir[fileExt] || 'Project';
362
- const packageDir = path.join(this.typeDir, formatDir, slug, versionNum);
430
+ let packageDir;
431
+ if (this.type === RegistryType.Plugins) {
432
+ // For plugins, use type-specific subdirectories
433
+ const formatDir = pluginFormatDir[fileExt] || 'Plugin';
434
+ packageDir = path.join(this.typeDir, formatDir, slug, versionNum);
435
+ }
436
+ else {
437
+ // For apps/projects/presets, files are in direct package directory
438
+ packageDir = path.join(this.typeDir, slug, versionNum);
439
+ }
363
440
  let fullPath;
364
441
  if (path.isAbsolute(openPath)) {
365
442
  fullPath = openPath;
@@ -434,9 +511,9 @@ export class ManagerLocal extends Manager {
434
511
  // Get local package file.
435
512
  const pkgFile = packageLoadFile(filePath);
436
513
  if (!pkgFile[type])
437
- return this.log(`Package ${type} is missing`);
514
+ throw new Error(`Package ${type} is missing`);
438
515
  if (!pkgFile[type][slug])
439
- return this.log(`Package ${type} ${slug} is not a dependency`);
516
+ throw new Error(`Package ${type} ${slug} is not a dependency`);
440
517
  // Uninstall dependency.
441
518
  const manager = new ManagerLocal(type, this.config.config);
442
519
  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.47",
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",