@open-audio-stack/core 0.1.44 → 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,22 +35,34 @@ 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
- if (args.operation === 'install') {
46
- await manager.install(args.id, args.version);
47
- }
48
- else if (args.operation === 'uninstall') {
49
- await manager.uninstall(args.id, args.version);
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
+ if (args.operation === 'install') {
47
+ await manager.install(args.id, args.version);
48
+ }
49
+ else if (args.operation === 'uninstall') {
50
+ await manager.uninstall(args.id, args.version);
51
+ }
52
+ else if (args.operation === 'installAll') {
53
+ await manager.installAll();
54
+ }
55
+ const result = { status: 'ok', code: 0 };
56
+ process.stdout.write('\n');
57
+ console.log(JSON.stringify(result));
58
+ process.exit(0);
50
59
  }
51
- else if (args.operation === 'installAll') {
52
- await manager.installAll();
60
+ catch (err) {
61
+ const message = err && err.message ? err.message : String(err);
62
+ const errorResult = { status: 'error', code: err && err.code ? err.code : 1, message };
63
+ process.stdout.write('\n');
64
+ console.error(JSON.stringify(errorResult));
65
+ process.exit(typeof errorResult.code === 'number' ? errorResult.code : 1);
53
66
  }
54
- console.log('ADMIN_COMPLETE');
55
67
  }
56
68
  adminInit();
@@ -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,16 +49,19 @@ 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,
51
56
  });
52
57
  }
53
58
  else if (ext === '.7z') {
54
- return unpack(filePath, dirPath, err => {
55
- if (err)
56
- throw new Error(`7z extraction failed: ${err.message}`);
59
+ return new Promise((resolve, reject) => {
60
+ unpack(filePath, dirPath, (err2) => {
61
+ if (err2)
62
+ return reject(new Error(`7z extraction failed: ${err2 && err2.message ? err2.message : String(err2)}`));
63
+ return resolve();
64
+ });
57
65
  });
58
66
  }
59
67
  }
@@ -247,14 +255,51 @@ export function fileMove(filePath, newPath) {
247
255
  return false;
248
256
  }
249
257
  export function filesMove(dirSource, dirTarget, dirSub, formatDir) {
250
- // Read all files from source directory, ignoring Mac Contents files.
251
- const allFiles = dirRead(`${dirSource}/**/*`);
252
- const files = allFiles.filter(f => !dirIs(f));
258
+ const filesAndFolders = dirRead(`${dirSource}/**/*`);
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
+ });
274
+ const files = filesAndFolders.filter(f => {
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).
282
+ if (!dirIs(f))
283
+ return true;
284
+ // Include bundle directories themselves (already identified above).
285
+ if (bundleDirs.has(f))
286
+ return true;
287
+ // Otherwise ignore.
288
+ return false;
289
+ });
253
290
  const filesMoved = [];
291
+ log('files', files);
254
292
  // For each file, move to correct folder based on type
255
293
  files.forEach((fileSource) => {
256
294
  const fileExt = path.extname(fileSource).slice(1).toLowerCase();
257
- 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
+ }
258
303
  // If this is not a supported file format, then ignore.
259
304
  if (fileExtTarget === undefined)
260
305
  return log(`${fileSource} - ${fileExt || 'no extension'} not mapped to a installation folder, skipping.`);
@@ -373,28 +418,46 @@ export function runCliAsAdmin(args) {
373
418
  const filename = fileURLToPath(import.meta.url).replace('src/', 'build/');
374
419
  const dirPathClean = dirname(filename).replace('app.asar', 'app.asar.unpacked');
375
420
  const script = path.join(dirPathClean, 'admin.js');
376
- // Temp file for logging
377
- const logFile = path.join(dirPathClean, 'admin.log');
378
- writeFileSync(logFile, ''); // Clear previous logs
379
421
  log(`Running as admin: node "${script}" ${args}`);
380
- // Monitor log file for completion
381
- let completed = false;
382
- const tail = spawn('tail', ['-f', logFile]);
383
- tail.stdout.on('data', data => {
384
- const output = data.toString();
385
- log(output);
386
- if (output.includes('ADMIN_COMPLETE')) {
387
- completed = true;
388
- tail.kill();
389
- resolve();
422
+ const cmd = `node "${script}" ${args}`;
423
+ sudoPrompt.exec(cmd, { name: 'Open Audio Stack' }, (error, stdout, stderr) => {
424
+ // Convert stdout/stderr buffers to strings for inspection
425
+ const stdoutStr = stdout ? (typeof stdout === 'string' ? stdout : stdout.toString()) : '';
426
+ const stderrStr = stderr ? (typeof stderr === 'string' ? stderr : stderr.toString()) : '';
427
+ const out = stdoutStr + stderrStr;
428
+ log(out);
429
+ // Try to parse structured JSON output from the admin script first.
430
+ // Admin script outputs JSON on its own line after a newline, so look for the last JSON object.
431
+ const lines = out.split('\n');
432
+ let jsonPayload = null;
433
+ for (let i = lines.length - 1; i >= 0; i--) {
434
+ const line = lines[i].trim();
435
+ if (!line)
436
+ continue; // Skip empty lines
437
+ try {
438
+ jsonPayload = JSON.parse(line);
439
+ break; // Found valid JSON, stop searching backwards
440
+ }
441
+ catch {
442
+ // This line is not JSON, continue searching
443
+ }
390
444
  }
391
- });
392
- // Run the script with sudoPrompt
393
- sudoPrompt.exec(`node "${script}" ${args} >> "${logFile}" 2>&1`, { name: 'Open Audio Stack' }, error => {
394
- if (error && !completed) {
395
- tail.kill();
396
- reject(error);
445
+ // If we found JSON output from admin script, prioritize it over sudoPrompt error
446
+ if (jsonPayload) {
447
+ if (jsonPayload && (jsonPayload.status === 'ok' || jsonPayload.code === 0)) {
448
+ return resolve();
449
+ }
450
+ const errMsg = jsonPayload && jsonPayload.message ? jsonPayload.message : JSON.stringify(jsonPayload);
451
+ return reject(new Error(`runCliAsAdmin: admin command reported error: ${errMsg}`));
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);
397
459
  }
460
+ return reject(new Error(`runCliAsAdmin: admin command did not report completion. stdout: ${stdoutStr} stderr: ${stderrStr}`));
398
461
  });
399
462
  });
400
463
  }
@@ -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.44",
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",