Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 171 additions & 83 deletions bin/asar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import packageJSON from '../package.json' with { type: 'json' };
import { createPackageWithOptions, listPackage, extractFile, extractAll } from '../lib/asar.js';
import { enableIntegrityDigestForApp, disableIntegrityDigestForApp, verifyIntegrityDigestForApp, printStoredIntegrityDigestForApp } from '../lib/integrity-digest.js';
import { program } from 'commander';
import { parseArgs } from 'node:util';
import fs from 'node:fs';
import path from 'node:path';

Expand All @@ -17,93 +17,181 @@ if (actualNodeVersion[0] < requiredNodeVersion[0] || (actualNodeVersion[0] === r
process.exit(1)
}

program.version('v' + packageJSON.version)
.description('Manipulate asar archive files')

program.command('pack <dir> <output>')
.alias('p')
.description('create asar archive')
.option('--ordering <file path>', 'path to a text file for ordering contents')
.option('--unpack <expression>', 'do not pack files matching glob <expression>')
.option('--unpack-dir <expression>', 'do not pack dirs matching glob <expression> or starting with literal <expression>')
.option('--exclude-hidden', 'exclude hidden files')
.action(function (dir, output, options) {
options = {
unpack: options.unpack,
unpackDir: options.unpackDir,
ordering: options.ordering,
version: options.sv,
arch: options.sa,
builddir: options.sb,
dot: !options.excludeHidden
const commands = {
pack: {
aliases: ['p'],
usage: 'pack|p [options] <dir> <output>',
description: 'create asar archive',
args: ['dir', 'output'],
options: {
ordering: { type: 'string', description: 'path to a text file for ordering contents' },
unpack: { type: 'string', description: 'do not pack files matching glob <expression>' },
'unpack-dir': { type: 'string', description: 'do not pack dirs matching glob <expression> or starting with literal <expression>' },
'exclude-hidden': { type: 'boolean', description: 'exclude hidden files' }
},
action: (positionals, values) => {
const [dir, output] = positionals
const options = {
unpack: values.unpack,
unpackDir: values['unpack-dir'],
ordering: values.ordering,
dot: !values['exclude-hidden']
}
return createPackageWithOptions(dir, output, options)
}
createPackageWithOptions(dir, output, options).catch(error => {
console.error(error)
process.exit(1)
})
})

program.command('list <archive>')
.alias('l')
.description('list files of asar archive')
.option('-i, --is-pack', 'each file in the asar is pack or unpack')
.action(function (archive, options) {
options = {
isPack: options.isPack
},
list: {
aliases: ['l'],
usage: 'list|l [options] <archive>',
description: 'list files of asar archive',
args: ['archive'],
options: {
'is-pack': { type: 'boolean', short: 'i', description: 'each file in the asar is pack or unpack' }
},
action: (positionals, values) => {
const [archive] = positionals
const files = listPackage(archive, { isPack: values['is-pack'] })
for (const i in files) {
console.log(files[i])
}
}
const files = listPackage(archive, options)
for (const i in files) {
console.log(files[i])
},
'extract-file': {
aliases: ['ef'],
usage: 'extract-file|ef <archive> <filename>',
description: 'extract one file from archive',
args: ['archive', 'filename'],
options: {},
action: (positionals) => {
const [archive, filename] = positionals
fs.writeFileSync(path.basename(filename), extractFile(archive, filename))
}
})

program.command('extract-file <archive> <filename>')
.alias('ef')
.description('extract one file from archive')
.action(function (archive, filename) {
fs.writeFileSync(path.basename(filename),
extractFile(archive, filename))
})

program.command('extract <archive> <dest>')
.alias('e')
.description('extract archive')
.action(function (archive, dest) {
extractAll(archive, dest)
})

program.command('integrity-digest <command> <app>')
.alias('id')
.description('manage integrity digest in app binary (macOS only)')
.action(async function (command, app) {
// No platform guard just in case users want to run this on other platforms
const allowedCommands = ['on', 'off', 'status', 'verify']
switch (command) {
case 'on':
await enableIntegrityDigestForApp(app)
break
case 'off':
await disableIntegrityDigestForApp(app)
break
case 'status':
await printStoredIntegrityDigestForApp(app)
break
case 'verify':
await verifyIntegrityDigestForApp(app)
break
default:
console.log('Unknown integrity digest command: %s. Allowed commands are: %s', command, allowedCommands.join(', '))
process.exit(1)
},
extract: {
aliases: ['e'],
usage: 'extract|e <archive> <dest>',
description: 'extract archive',
args: ['archive', 'dest'],
options: {},
action: (positionals) => {
const [archive, dest] = positionals
extractAll(archive, dest)
}
})
},
'integrity-digest': {
aliases: ['id'],
usage: 'integrity-digest|id <command> <app>',
description: 'manage integrity digest in app binary (macOS only)',
args: ['command', 'app'],
options: {},
action: async (positionals) => {
const [command, app] = positionals
// No platform guard just in case users want to run this on other platforms
const allowedCommands = ['on', 'off', 'status', 'verify']
switch (command) {
case 'on':
await enableIntegrityDigestForApp(app)
break
case 'off':
await disableIntegrityDigestForApp(app)
break
case 'status':
await printStoredIntegrityDigestForApp(app)
break
case 'verify':
await verifyIntegrityDigestForApp(app)
break
default:
console.log('Unknown integrity digest command: %s. Allowed commands are: %s', command, allowedCommands.join(', '))
process.exit(1)
}
}
}
}

function printHelp() {
console.log('Usage: asar [options] [command]')
console.log()
console.log('Manipulate asar archive files')
console.log()
console.log('Options:')
console.log(' -V, --version output the version number')
console.log(' -h, --help display help for command')
console.log()
console.log('Commands:')
for (const [name, cmd] of Object.entries(commands)) {
const label = `${name}|${cmd.aliases[0]}`
console.log(` ${label.padEnd(32)} ${cmd.description}`)
}
}

function printCommandHelp(cmd) {
console.log(`Usage: asar ${cmd.usage}`)
console.log()
console.log(cmd.description)
console.log()
console.log('Options:')
for (const [opt, spec] of Object.entries(cmd.options)) {
const prefix = spec.short ? `-${spec.short}, ` : ''
const suffix = spec.type === 'string' ? ' <value>' : ''
const label = `${prefix}--${opt}${suffix}`
console.log(` ${label.padEnd(32)} ${spec.description}`)
}
console.log(` ${'-h, --help'.padEnd(32)} display help for command`)
}

program.command('*', { hidden: true})
.action(function (_cmd, args) {
console.log('asar: \'%s\' is not an asar command. See \'asar --help\'.', args[0])
})
const args = process.argv.slice(2)

program.parse(process.argv)
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
printHelp()
process.exit(0)
}

if (args[0] === '--version' || args[0] === '-V') {
console.log('v' + packageJSON.version)
process.exit(0)
}

if (program.args.length === 0) {
program.help()
const commandName = args[0]
const commandArgs = args.slice(1)

let command = commands[commandName]
if (!command) {
command = Object.values(commands).find(cmd => cmd.aliases.includes(commandName))
}

if (!command) {
console.log('asar: \'%s\' is not an asar command. See \'asar --help\'.', commandName)
process.exit(1)
}

let values, positionals
try {
({ values, positionals } = parseArgs({
args: commandArgs,
options: {
...command.options,
help: { type: 'boolean', short: 'h' }
},
allowPositionals: true
}))
} catch (error) {
console.error(`error: ${error.message}`)
process.exit(1)
}

if (values.help) {
printCommandHelp(command)
process.exit(0)
}

if (positionals.length < command.args.length) {
const missing = command.args[positionals.length]
console.error(`error: missing required argument '${missing}'`)
process.exit(1)
}

Promise.resolve(command.action(positionals, values)).catch(error => {
console.error(error)
process.exit(1)
})
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"prepare": "husky"
},
"dependencies": {
"commander": "^13.1.0",
"glob": "^13.0.2",
"minimatch": "^10.0.1",
"plist": "^3.1.0"
Expand Down
8 changes: 0 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ __metadata:
"@types/node": "npm:^22.12.0"
"@types/plist": "npm:^3.0.5"
"@types/semver": "npm:^7.7.1"
commander: "npm:^13.1.0"
electron: "npm:^35.7.5"
glob: "npm:^13.0.2"
husky: "npm:^9.1.7"
Expand Down Expand Up @@ -1036,13 +1035,6 @@ __metadata:
languageName: node
linkType: hard

"commander@npm:^13.1.0":
version: 13.1.0
resolution: "commander@npm:13.1.0"
checksum: 10c0/7b8c5544bba704fbe84b7cab2e043df8586d5c114a4c5b607f83ae5060708940ed0b5bd5838cf8ce27539cde265c1cbd59ce3c8c6b017ed3eec8943e3a415164
languageName: node
linkType: hard

"commander@npm:^14.0.2":
version: 14.0.3
resolution: "commander@npm:14.0.3"
Expand Down
Loading