2 Commits

Author SHA1 Message Date
a1338aa9bc 3.0.1: detect gpg password cache 2025-01-27 01:16:50 -05:00
5e105efd9d add completion scripts 2025-01-26 19:11:46 -05:00
7 changed files with 230 additions and 30 deletions

99
completion/bash Normal file
View File

@@ -0,0 +1,99 @@
#/usr/bin/env bash
LIBDIR=${LIBDIR:-'/usr/share/artools/lib'}
_artixpkg_pkgbase() {
source "${LIBDIR}"/pkg/git/config.sh
source "${LIBDIR}"/pkg/util.sh
ls -1 "${TREE_DIR_ARTIX}" | tr '\n' ' '
}
_artix_metro_completion() {
local cur prev comps repos autorepos comp_cword_exflag
source "${LIBDIR}"/pkg/db/db.sh 2>/dev/null
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
for ((i = COMP_CWORD - 1; i >= 0; i--)); do
if [[ ${COMP_WORDS[i]} != -* ]]; then
last_non_flag_word="${COMP_WORDS[i]}"
break
fi
done
comps=""
comp_cword_exflag=0
comp_cword_all=0
for ((i = 0; i < ${#COMP_WORDS[@]} - 1; i++)); do
word="${COMP_WORDS[i]}"
comps_all+=" $word"
((comp_cword_all++))
if [[ $word != -* ]]; then
comps+=" $word"
((comp_cword_exflag++))
fi
done
comps="${comps:1}"
repos=""
for word in "${ARTIX_DB[@]}"; do
if [[ $word != -* ]]; then
repos+=" $word"
fi
done
repos="${repos:1}"
autorepos=""
for word in "${ARTIX_DB_MAP[@]}"; do
if [[ $word != -* ]]; then
autorepos+=" $word"
fi
done
autorepos="${autorepos:1}"
case "${prev}" in
"--token")
# this flag expects a parameter
COMPREPLY=()
;;
"-j"|"--job")
COMPREPLY=( $(compgen -f -- "$cur") )
;;
"--workspace")
COMPREPLY=( $(compgen -d -- "$cur") )
;;
"--start")
COMPREPLY=($(compgen -W "$(_artixpkg_pkgbase)" -- ${cur}))
;;
*)
local metroCommon="-h --help --start --token --workspace --increment "
case "${comps}" in
"artix-metro add"*)
case "${comp_cword_exflag}" in
2)
COMPREPLY=($(compgen -W "$metroCommon $autorepos $repos" -- ${cur}))
;;
*)
COMPREPLY=($(compgen -W "$metroCommon $(_artixpkg_pkgbase)" -- ${cur}))
;;
esac
;;
"artix-metro move"*)
case "${comp_cword_exflag}" in
2|3)
COMPREPLY=($(compgen -W "$metroCommon $autorepos $repos" -- ${cur}))
;;
*)
COMPREPLY=($(compgen -W "$metroCommon $(_artixpkg_pkgbase)" -- ${cur}))
;;
esac
;;
*)
COMPREPLY=($(compgen -W "$metroCommon -j --job add move" -- ${cur}))
;;
esac
;;
esac
}
complete -F _artix_metro_completion artix-metro

61
completion/zsh Normal file
View File

@@ -0,0 +1,61 @@
# Load necessary library files
LIBDIR=${LIBDIR:-'/usr/share/artools/lib'}
_artix_metro_completion() {
local -a metroCommon repos autorepos pkgbase
local curcontext="$curcontext" state
# Load external configurations
source "${LIBDIR}/pkg/db/db.sh" 2>/dev/null
# Common options
metroCommon=("-h" "--help" "--start" "--token" "--workspace" "--increment" "-j" "--job")
# Populate variables
repos=("${(s: :)ARTIX_DB}")
autorepos=("${(s: :)ARTIX_DB_MAP}")
pkgbase=("package") # TODO: populate cloned packages
# Handle command and argument contexts
_arguments -C \
'--token[Provide a token]: ' \
'-j[Specify a job]: :_files' \
'--job[Specify a job]: :_files' \
'--workspace[Specify a workspace]: :_files -/' \
'--start[Start a process]:pkgbase:(${pkgbase})' \
'1:command:(${metroCommon} add move)' \
'2:repo:(${metroCommon} ${autorepos} ${repos})' \
'*:pkgbase:->pkgbase'
# Contextual argument handling
case $state in
pkgbase)
case $words[2] in
add)
if (( CURRENT == 3 )); then
# First argument after "add" is a repo
_values "repo" "${metroCommon[@]}" "${autorepos[@]}" "${repos[@]}"
else
# Remaining arguments are pkgbase
_values "pkgbase" "${pkgbase[@]}"
fi
;;
move)
if (( CURRENT == 3 )); then
# First repo for "move"
_values "repo" "${metroCommon[@]}" "${autorepos[@]}" "${repos[@]}"
elif (( CURRENT == 4 )); then
# Second repo for "move"
_values "repo" "${metroCommon[@]}" "${autorepos[@]}" "${repos[@]}"
else
# Remaining arguments are pkgbase
_values "pkgbase" "${pkgbase[@]}"
fi
;;
esac
;;
esac
}
# Register the completion function for artix-metro
compdef _artix_metro_completion artix-metro

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "artix-metro",
"version": "3.0.0",
"version": "3.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "artix-metro",
"version": "3.0.0",
"version": "3.0.1",
"license": "MIT",
"dependencies": {
"artix-checkupdates": "1.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "artix-metro",
"version": "3.0.0",
"version": "3.0.1",
"description": "Automate pushing packages to Artix",
"keywords": [
"artix",
@@ -13,7 +13,8 @@
},
"files": [
"distribution",
"bin"
"bin",
"completion"
],
"author": {
"name": "Cory Sanin",

View File

@@ -4,6 +4,7 @@ import clc from 'cli-color';
import JSON5 from 'json5';
import { Writable } from 'stream';
import { Pusher } from './pusher.mjs';
import { isPasswordRequired } from './runCommand.mjs';
import type { Job, ArtixpkgRepo } from './pusher.mts';
/**
@@ -11,6 +12,9 @@ import type { Job, ArtixpkgRepo } from './pusher.mts';
* @returns a promise that resolves the password
*/
async function getGpgPass() {
if ((process.env['SKIPGPGPASSPROMPT'] || '').toLowerCase() === 'true') {
return 'SKIP';
}
let muted = false;
let mutableStdout = new Writable({
write: function (chunk, encoding, callback) {
@@ -20,6 +24,11 @@ async function getGpgPass() {
callback();
}
});
if (! await isPasswordRequired()) {
console.log(clc.green('Looks like GPG agent is currently running and password is cached. '
+ 'If there is no timeout on your cached password, you can simply press enter.\n'
+ 'To skip this GPG password prompt next time, set $SKIPGPGPASSPROMPT to true'));
}
let rl = readline.createInterface({
input: process.stdin,
output: mutableStdout,

View File

@@ -4,11 +4,11 @@ import * as readline from 'node:readline/promises';
import clc from 'cli-color';
import path from 'node:path';
import os from 'node:os';
import { spawn } from 'node:child_process';
import { Checkupdates } from 'artix-checkupdates';
import { Gitea } from './gitea.mjs'
import { ArtoolsConfReader, DefaultConf } from './artoolsconf.mjs';
import { snooze } from './snooze.mjs';
import { runCommand, isPasswordRequired } from './runCommand.mjs';
import type { ArtixRepo } from 'artix-checkupdates';
import type { ArtoolsConf } from './artoolsconf.mts';
@@ -26,22 +26,8 @@ interface Job extends Partial<ArtoolsConf> {
}
const PACKAGE_ORG = 'packages';
const SIGNATUREEXPIRY = 30000;//in ms
const SIGNFILE = path.join(os.tmpdir(), 'signfile');
/**
* Run a command (as a promise).
* @param command command to run
* @param args args to pass
* @returns true if success
*/
function runCommand(command: string, args: string[] = []): Promise<boolean> {
return new Promise((res, _) => {
let proc = spawn(command, args, { stdio: ['ignore', process.stdout, process.stderr] });
proc.on('exit', code => res(code === 0));
});
}
/**
* Formats text to be sent as a parameter to some command
* @param param
@@ -54,15 +40,16 @@ function escapeCommandParam(param: string) {
class Pusher {
private _gitea: Gitea | null;
private _lastSign: number = 0;
private _config: PusherConfig;
private _artools: ArtoolsConf;
private _constructed: Promise<void>;
private _createdSignfile: boolean;
constructor(config: PusherConfig = {}) {
this._gitea = null;
this._artools = DefaultConf
this._config = config;
this._createdSignfile = false;
this._constructed = (async () => {
try {
this._artools = await (new ArtoolsConfReader()).readConf();
@@ -78,13 +65,11 @@ class Pusher {
}
async refreshGpg() {
let currentTime = (new Date()).getTime();
if (this._config.gpgpass && currentTime - this._lastSign > SIGNATUREEXPIRY) {
if (await isPasswordRequired()) {
console.log(clc.cyan('Refreshing signature...'));
await runCommand('touch', [SIGNFILE]);
await runCommand('gpg', ['-a', '--passphrase', escapeCommandParam(this._config.gpgpass), '--batch', '--pinentry-mode', 'loopback', '--detach-sign', SIGNFILE]);
this._createdSignfile ||= await runCommand('touch', [SIGNFILE]);
await runCommand('gpg', ['-a', '--passphrase', escapeCommandParam(this._config.gpgpass || ''), '--batch', '--pinentry-mode', 'loopback', '--detach-sign', SIGNFILE]);
await fsp.rm(`${SIGNFILE}.asc`);
this._lastSign = currentTime;
}
}
@@ -214,11 +199,13 @@ class Pusher {
}
}
console.log(clc.greenBright('SUCCESS: All packages built'));
try {
await fsp.rm(SIGNFILE);
}
catch {
console.error(clc.red('failed to remove temp signfile'));
if (this._createdSignfile) {
try {
await fsp.rm(SIGNFILE);
}
catch {
console.error(clc.red('failed to remove temp signfile'));
}
}
}
}

43
src/runCommand.mts Normal file
View File

@@ -0,0 +1,43 @@
import { spawn } from 'node:child_process';
import type { SpawnOptions } from 'node:child_process';
/**
* Run a command (as a promise).
* @param command command to run
* @param args args to pass
* @returns promise that yields true if success
*/
function runCommand(command: string, args: string[] = [], stdOutToLogs: boolean = true): Promise<boolean> {
return new Promise((res, _) => {
const opts: SpawnOptions = {stdio: stdOutToLogs ? ['pipe', 'inherit', 'inherit'] : 'pipe'};
const proc = spawn(command, args, opts);
proc.on('exit', code => res(code === 0));
});
}
/**
* Check if password input is necessary for signing
* @returns promise that yieds true if password is required
*/
function isPasswordRequired(): Promise<boolean> {
return new Promise(async (res, _) => {
if (! await runCommand('gpg-agent', [], false)) {
return res(true);
}
const proc = spawn('gpg-connect-agent', ['KEYINFO --list', '/bye'], { stdio: 'pipe' });
let outputstr = '';
proc.stdout.on('data', data => {
outputstr += data.toString();
});
proc.on('exit', async () => {
const keyinfo = outputstr.split('\n').filter(l => l.includes('KEYINFO'));
res(!keyinfo.find(l => {
const tokens = l.split(' ');
return tokens[0] === 'S' && tokens[1] === 'KEYINFO' && tokens[3] === 'D' && tokens[6] === '1';
}));
});
});
}
export default runCommand;
export { runCommand, isPasswordRequired };