Skip to content

Commit a8feca2

Browse files
authored
Merge pull request #55 from open-audio-stack/fix/install-issues
Fix/install issues
2 parents 853395a + 721bcd1 commit a8feca2

10 files changed

Lines changed: 264 additions & 56 deletions

File tree

package-lock.json

Lines changed: 36 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@open-audio-stack/core",
3-
"version": "0.1.45",
3+
"version": "0.1.47",
44
"description": "Open-source audio plugin management software",
55
"type": "module",
66
"main": "./build/index.js",
@@ -42,6 +42,7 @@
4242
"@types/adm-zip": "^0.5.5",
4343
"@types/fs-extra": "^11.0.4",
4444
"@types/js-yaml": "^4.0.9",
45+
"@types/mime-types": "^3.0.1",
4546
"@types/node": "^22.7.8",
4647
"@types/semver": "^7.5.8",
4748
"@vitest/coverage-v8": "^3.0.5",
@@ -62,6 +63,7 @@
6263
"glob": "^11.0.0",
6364
"inquirer": "^12.4.1",
6465
"js-yaml": "^4.1.0",
66+
"mime-types": "^3.0.2",
6567
"semver": "^7.6.3",
6668
"slugify": "^1.6.6",
6769
"tar": "^7.4.3",

specification.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ $ manager config get registries
133133
]
134134
```
135135

136+
#### Registry versioning (optional)
137+
138+
Registries can expose versioned endpoints to avoid breaking changes when introducing new features. When a registry is versioned, managers should append the version segment to the registry root when requesting resources.
139+
140+
Example: Registry root `https://example.com/registry` with version `v1` → fetch plugin list at `https://example.com/registry/v1/plugins`.
141+
142+
Versioning is optional — if Managers call the root url, they will get the latest version by default.
143+
136144
### App directory
137145

138146
Defaults to manager installation directory.
@@ -437,13 +445,15 @@ Create new package metadata:
437445
- If package version not found return error
438446
- Check to see if package is installed:
439447
- If not installed, return error
440-
- Filter package files that match the current architecture and system
441-
- Find a file with an `open` field defined:
442-
- If no compatible file with `open` field found, return error
443-
- Execute the file/command specified in the file's `open` field with any additional options
448+
- Filter package `files` entries that match the current architecture and system
449+
- Find a `files` entry that includes an `open` field and matches the system/architecture:
450+
- If no compatible `files` entry with an `open` field is found, return error
451+
- Execute the file/command specified in that `files` entry's `open` field.
452+
453+
Note: The manager will use the `open` field defined in the package metadata (per-file) to determine the correct entry point for the target system.
444454

445455
Open any package by slug and version:
446-
`$ manager <registryType> open <slug>@<version> <options>`
456+
`$ manager <registryType> open <slug>@<version>`
447457

448458
## Project
449459

src/classes/ManagerLocal.ts

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
dirCreate,
88
dirDelete,
99
dirEmpty,
10+
dirIs,
1011
dirMove,
1112
dirRead,
1213
fileCreate,
1314
fileCreateJson,
1415
fileCreateYaml,
16+
fileExec,
1517
fileExists,
1618
fileHash,
1719
fileInstall,
@@ -32,8 +34,8 @@ import { PluginFormat, pluginFormatDir } from '../types/PluginFormat.js';
3234
import { ConfigInterface } from '../types/Config.js';
3335
import { ConfigLocal } from './ConfigLocal.js';
3436
import { packageCompatibleFiles } from '../helpers/package.js';
35-
import { PresetFormat, presetFormatDir } from '../types/PresetFormat.js';
36-
import { ProjectFormat, projectFormatDir } from '../types/ProjectFormat.js';
37+
import { presetFormatDir } from '../types/PresetFormat.js';
38+
import { projectFormatDir } from '../types/ProjectFormat.js';
3739
import { FileFormat } from '../types/FileFormat.js';
3840
import { licenses } from '../types/License.js';
3941
import { PluginType, PluginTypeOption, pluginTypes } from '../types/PluginType.js';
@@ -297,12 +299,77 @@ export class ManagerLocal extends Manager {
297299
dirMove(dirSource, dirTarget);
298300
fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion);
299301
} else {
300-
// Move only supported file extensions into their respective installation directories.
301-
const filesMoved: string[] = filesMove(dirSource, this.typeDir, dirSub, formatDir);
302-
filesMoved.forEach((fileMoved: string) => {
303-
const fileJson: string = path.join(path.dirname(fileMoved), 'index.json');
304-
fileCreateJson(fileJson, pkgVersion);
302+
// Check if archive contains installer files (pkg, dmg) that should be run
303+
const allFiles = dirRead(`${dirSource}/**/*`).filter(f => !dirIs(f));
304+
const installerFiles = allFiles.filter(f => {
305+
const ext = path.extname(f).toLowerCase();
306+
return ext === '.pkg' || ext === '.dmg';
305307
});
308+
309+
if (installerFiles.length > 0) {
310+
// Run installer files found in archive
311+
for (const installerFile of installerFiles) {
312+
if (isTests()) fileOpen(installerFile);
313+
else fileInstall(installerFile);
314+
}
315+
// Create directory and save package info for installer
316+
const dirTarget: string = path.join(this.typeDir, 'Installers', dirSub);
317+
dirCreate(dirTarget);
318+
fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion);
319+
} else if (this.type === RegistryType.Plugins) {
320+
// For plugins, move files into type-specific subdirectories
321+
const filesMoved: string[] = filesMove(dirSource, this.typeDir, dirSub, formatDir);
322+
if (filesMoved.length === 0) {
323+
throw new Error(`No compatible files found to install for ${slug}`);
324+
}
325+
filesMoved.forEach((fileMoved: string) => {
326+
const fileJson: string = path.join(path.dirname(fileMoved), 'index.json');
327+
fileCreateJson(fileJson, pkgVersion);
328+
});
329+
} else {
330+
// For apps/projects/presets, move entire directory without type subdirectories
331+
const dirTarget: string = path.join(this.typeDir, dirSub);
332+
dirCreate(dirTarget);
333+
dirMove(dirSource, dirTarget);
334+
fileCreateJson(path.join(dirTarget, 'index.json'), pkgVersion);
335+
// Ensure executable permissions for likely executables inside moved app/project/preset
336+
try {
337+
const movedFiles = dirRead(path.join(dirTarget, '**', '*')).filter(f => !dirIs(f));
338+
movedFiles.forEach((movedFile: string) => {
339+
const ext = path.extname(movedFile).slice(1).toLowerCase();
340+
if (['', 'elf', 'exe'].includes(ext)) {
341+
try {
342+
fileExec(movedFile);
343+
} catch (err) {
344+
this.log(`Failed to set exec on ${movedFile}:`, err);
345+
}
346+
}
347+
});
348+
} catch (err) {
349+
this.log('Error while setting executable permissions:', err);
350+
}
351+
// Also handle macOS .app bundles: set exec on binaries in Contents/MacOS
352+
try {
353+
const appDirs = dirRead(path.join(dirTarget, '**', '*.app')).filter(d => dirIs(d));
354+
appDirs.forEach((appDir: string) => {
355+
try {
356+
const macosBinPattern = path.join(appDir, 'Contents', 'MacOS', '**', '*');
357+
const macosFiles = dirRead(macosBinPattern).filter(f => !dirIs(f));
358+
macosFiles.forEach((binFile: string) => {
359+
try {
360+
fileExec(binFile);
361+
} catch (err) {
362+
this.log(`Failed to set exec on app binary ${binFile}:`, err);
363+
}
364+
});
365+
} catch (err) {
366+
this.log(`Error scanning .app contents for ${appDir}:`, err);
367+
}
368+
});
369+
} catch (err) {
370+
this.log(err);
371+
}
372+
}
306373
}
307374
}
308375
}
@@ -320,9 +387,9 @@ export class ManagerLocal extends Manager {
320387
}
321388

322389
// Loop through all packages and install each one.
323-
for (const [slug, pkg] of this.packages) {
390+
for (const pkg of this.listPackages()) {
324391
const versionNum: string = pkg.latestVersion();
325-
await this.install(slug, versionNum);
392+
await this.install(pkg.slug, versionNum);
326393
}
327394
return this.listPackages();
328395
}
@@ -333,14 +400,16 @@ export class ManagerLocal extends Manager {
333400
await manager.sync();
334401
manager.scan();
335402
const pkg: Package | undefined = manager.getPackage(slug);
336-
if (!pkg) return this.log(`Package ${slug} not found in registry`);
403+
if (!pkg) throw new Error(`Package ${slug} not found in registry`);
337404
const versionNum: string = version || pkg.latestVersion();
338405
const pkgVersion: PackageVersion | undefined = pkg?.getVersion(versionNum);
339-
if (!pkgVersion) return this.log(`Package ${slug} version ${versionNum} not found in registry`);
406+
if (!pkgVersion) throw new Error(`Package ${slug} version ${versionNum} not found in registry`);
340407
// Get local package file.
341408
const pkgFile = packageLoadFile(filePath) as any;
342409
if (pkgFile[type] && pkgFile[type][slug] && pkgFile[type][slug] === versionNum) {
343-
return this.log(`Package ${slug} version ${versionNum} is already a dependency`);
410+
this.log(`Package ${slug} version ${versionNum} is already a dependency`);
411+
pkgFile.installed = true;
412+
return pkgFile;
344413
}
345414
// Install dependency.
346415
await manager.install(slug, version);
@@ -396,11 +465,16 @@ export class ManagerLocal extends Manager {
396465
try {
397466
const openPath = (openableFile as any).open;
398467
const fileExt: string = path.extname(openPath).slice(1).toLowerCase();
399-
let formatDir: string = pluginFormatDir[fileExt as PluginFormat] || 'Plugin';
400-
if (this.type === RegistryType.Apps) formatDir = pluginFormatDir[fileExt as PluginFormat] || 'App';
401-
else if (this.type === RegistryType.Presets) formatDir = presetFormatDir[fileExt as PresetFormat] || 'Preset';
402-
else if (this.type === RegistryType.Projects) formatDir = projectFormatDir[fileExt as ProjectFormat] || 'Project';
403-
const packageDir = path.join(this.typeDir, formatDir, slug, versionNum);
468+
let packageDir: string;
469+
470+
if (this.type === RegistryType.Plugins) {
471+
// For plugins, use type-specific subdirectories
472+
const formatDir: string = pluginFormatDir[fileExt as PluginFormat] || 'Plugin';
473+
packageDir = path.join(this.typeDir, formatDir, slug, versionNum);
474+
} else {
475+
// For apps/projects/presets, files are in direct package directory
476+
packageDir = path.join(this.typeDir, slug, versionNum);
477+
}
404478
let fullPath: string;
405479
if (path.isAbsolute(openPath)) {
406480
fullPath = openPath;
@@ -471,8 +545,8 @@ export class ManagerLocal extends Manager {
471545
async uninstallDependency(slug: string, version?: string, filePath?: string, type = RegistryType.Plugins) {
472546
// Get local package file.
473547
const pkgFile = packageLoadFile(filePath) as any;
474-
if (!pkgFile[type]) return this.log(`Package ${type} is missing`);
475-
if (!pkgFile[type][slug]) return this.log(`Package ${type} ${slug} is not a dependency`);
548+
if (!pkgFile[type]) throw new Error(`Package ${type} is missing`);
549+
if (!pkgFile[type][slug]) throw new Error(`Package ${type} ${slug} is not a dependency`);
476550

477551
// Uninstall dependency.
478552
const manager = new ManagerLocal(type, this.config.config);

src/classes/Package.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export class Package extends Base {
2929
...(recs.length > 0 && { recs }),
3030
};
3131
if (Object.keys(report).length > 0) this.reports.set(num, report);
32-
if (errors.length > 0) return this.log(`Package ${version.name} version ${num} errors`, errors);
32+
if (errors.length > 0)
33+
throw new Error(`Package ${version.name} version ${num} has validation errors: ${JSON.stringify(errors)}`);
3334
version.verified = packageIsVerified(this.slug, version);
3435
this.versions.set(num, version);
3536
this.version = this.latestVersion();

src/helpers/admin.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ export function adminArguments(): Arguments {
4242
}
4343

4444
export async function adminInit() {
45-
const args: Arguments = adminArguments();
46-
const manager = new ManagerLocal(args.type, { appDir: args.appDir });
47-
if (args.log) manager.logEnable();
48-
manager.log('adminInit', args);
49-
await manager.sync();
50-
manager.scan();
5145
try {
46+
const args: Arguments = adminArguments();
47+
const manager = new ManagerLocal(args.type, { appDir: args.appDir });
48+
if (args.log) manager.logEnable();
49+
manager.log('adminInit', args);
50+
await manager.sync();
51+
manager.scan();
5252
if (args.operation === 'install') {
5353
await manager.install(args.id, args.version);
5454
} else if (args.operation === 'uninstall') {
@@ -64,7 +64,7 @@ export async function adminInit() {
6464
const message = err && err.message ? err.message : String(err);
6565
const errorResult = { status: 'error', code: err && err.code ? err.code : 1, message };
6666
process.stdout.write('\n');
67-
console.log(JSON.stringify(errorResult));
67+
console.error(JSON.stringify(errorResult));
6868
process.exit(typeof errorResult.code === 'number' ? errorResult.code : 1);
6969
}
7070
}

0 commit comments

Comments
 (0)