Add custom path support for cli/tui in terminal config
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"dev": "SYNC_SERVER_URL=https://sync.xo.harvmaster.com tsx src/index.ts",
|
"dev": "SYNC_SERVER_URL=https://sync.xo.harvmaster.com tsx src/index.ts",
|
||||||
"build": "tsc && npm run build:copy-scripts",
|
"build": "tsc && npm run build:copy-scripts",
|
||||||
"build:copy-scripts": "cp -r src/cli/autocomplete/scripts dist/cli/autocomplete/",
|
"build:copy-scripts": "cp -r src/cli/autocomplete/scripts dist/cli/autocomplete/",
|
||||||
|
"build:unsafe": "tsc --nocheck --noEmitOnError false || true && npm run build:copy-scripts",
|
||||||
"start": "SYNC_SERVER_URL=https://sync.xo.harvmaster.com node dist/index.js",
|
"start": "SYNC_SERVER_URL=https://sync.xo.harvmaster.com node dist/index.js",
|
||||||
"test": "vitest --run --passWithNoTests",
|
"test": "vitest --run --passWithNoTests",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ npm install -g .
|
|||||||
|
|
||||||
### Install autocomplete completions (From the xo-cli directory)
|
### Install autocomplete completions (From the xo-cli directory)
|
||||||
|
|
||||||
|
These commands add `XO_CONFIG_DIR` to your shell config with a default of
|
||||||
|
`~/.config/xo-cli`. Set it to an absolute path before installing, or edit the
|
||||||
|
generated assignment, to use a different wallet-state directory.
|
||||||
|
|
||||||
#### Install for bash
|
#### Install for bash
|
||||||
```bash
|
```bash
|
||||||
npm run autocomplete:install:bash
|
npm run autocomplete:install:bash
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ There are two global commands after install:
|
|||||||
|
|
||||||
## Global config directory
|
## Global config directory
|
||||||
|
|
||||||
Wallet state lives under **`~/.config/xo-cli/`** (XDG-style), so you can run commands from any directory:
|
Wallet state lives under **`${XO_CONFIG_DIR:-~/.config/xo-cli}`**, so you can run commands from any directory. Set `XO_CONFIG_DIR` to use a different wallet-state root.
|
||||||
|
|
||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
| ----------------------------- | ----------------------------------------------------------------------- |
|
| -------------------------- | ----------------------------------------------------------------------- |
|
||||||
| `~/.config/xo-cli/mnemonics/` | Mnemonic files (`mnemonic-*`) |
|
| `$XO_CONFIG_DIR/mnemonics/` | Mnemonic files (`mnemonic-*`) |
|
||||||
| `~/.config/xo-cli/data/` | Engine DB (`xo-wallet.db`) and invitation storage (`xo-invitations.db`) |
|
| `$XO_CONFIG_DIR/data/` | Engine DB (`xo-wallet.db`) and invitation storage (`xo-invitations.db`) |
|
||||||
| `~/.config/xo-cli/.wallet` | JSON settings (`default-mnemonic`, `currency`) |
|
| `$XO_CONFIG_DIR/.wallet` | JSON settings (`default-mnemonic`, `currency`) |
|
||||||
|
|
||||||
**Local to your shell’s current directory:** template JSON paths, invitation JSON you create/import, and any path you pass explicitly (e.g. `-m /abs/path/to/file`).
|
**Local to your shell’s current directory:** template JSON paths, invitation JSON you create/import, and any path you pass explicitly (e.g. `-m /abs/path/to/file`).
|
||||||
|
|
||||||
@@ -39,21 +39,24 @@ npx tsx src/cli/index.ts <command> [options]
|
|||||||
npx tsx src/index.ts # TUI
|
npx tsx src/index.ts # TUI
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment variables (TUI / `xo-tui`)
|
### Environment variables
|
||||||
|
|
||||||
| Variable | Default |
|
| Variable | Default |
|
||||||
| ------------------------- | ----------------------------------------- |
|
| ------------------------- | ----------------------------------------- |
|
||||||
|
| `XO_CONFIG_DIR` | `~/.config/xo-cli` |
|
||||||
| `SYNC_SERVER_URL` | `http://localhost:3000` |
|
| `SYNC_SERVER_URL` | `http://localhost:3000` |
|
||||||
| `DB_PATH` | `~/.config/xo-cli/data` |
|
| `DB_PATH` | `$XO_CONFIG_DIR/data` |
|
||||||
| `DB_FILENAME` | `xo-wallet.db` |
|
| `DB_FILENAME` | `xo-wallet.db` |
|
||||||
| `INVITATION_STORAGE_PATH` | `~/.config/xo-cli/data/xo-invitations.db` |
|
| `INVITATION_STORAGE_PATH` | `$XO_CONFIG_DIR/data/xo-invitations.db` |
|
||||||
|
|
||||||
|
Use an absolute path for a custom root. Setting `XO_CONFIG_DIR` does not copy state from the default directory.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Wallet Setup
|
### Wallet Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate a new mnemonic (saved under ~/.config/xo-cli/mnemonics/)
|
# Generate a new mnemonic (saved under $XO_CONFIG_DIR/mnemonics/)
|
||||||
xo-cli mnemonic create
|
xo-cli mnemonic create
|
||||||
|
|
||||||
# Import an existing mnemonic seed phrase
|
# Import an existing mnemonic seed phrase
|
||||||
@@ -68,7 +71,7 @@ xo-cli mnemonic list
|
|||||||
### Wallet Persistence
|
### Wallet Persistence
|
||||||
|
|
||||||
The first time you pass `-m <name>`, that reference is saved as
|
The first time you pass `-m <name>`, that reference is saved as
|
||||||
`default-mnemonic` in `~/.config/xo-cli/.wallet`. Later runs can omit `-m`.
|
`default-mnemonic` in `$XO_CONFIG_DIR/.wallet`. Later runs can omit `-m`.
|
||||||
|
|
||||||
`currency` controls the fiat unit used when showing BCH/sats conversions in the TUI.
|
`currency` controls the fiat unit used when showing BCH/sats conversions in the TUI.
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ Mnemonic resolution order:
|
|||||||
|
|
||||||
1. Absolute path, if the file exists
|
1. Absolute path, if the file exists
|
||||||
2. Path relative to the current working directory
|
2. Path relative to the current working directory
|
||||||
3. `~/.config/xo-cli/mnemonics/<basename>`
|
3. `$XO_CONFIG_DIR/mnemonics/<basename>`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xo-cli resource list -m mnemonic-nuclear
|
xo-cli resource list -m mnemonic-nuclear
|
||||||
@@ -93,7 +96,7 @@ xo-cli resource list
|
|||||||
| `-v`, `--verbose` | Verbose output |
|
| `-v`, `--verbose` | Verbose output |
|
||||||
| `-h`, `--help` | Help |
|
| `-h`, `--help` | Help |
|
||||||
|
|
||||||
Advanced: you can pass `--database-path`, `--database-filename`, and `--invitation-storage-path` to override the defaults under `~/.config/xo-cli/data/` (see `src/cli/index.ts`).
|
Advanced: you can pass `--database-path`, `--database-filename`, and `--invitation-storage-path` to override the defaults under `$XO_CONFIG_DIR/data/` (see `src/cli/index.ts`).
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -201,9 +204,11 @@ eval "$(xo-cli completions zsh)"
|
|||||||
xo-cli completions fish | source
|
xo-cli completions fish | source
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`xo-cli completions <shell> --install` adds a default `XO_CONFIG_DIR` assignment to the shell startup file if one is not already present. Mnemonic aliases are completed directly from `$XO_CONFIG_DIR/mnemonics/`; database-backed suggestions still use `xo-complete`.
|
||||||
|
|
||||||
## File Conventions
|
## File Conventions
|
||||||
|
|
||||||
| Location | Purpose |
|
| Location | Purpose |
|
||||||
| ------------------- | ------------------------------------------ |
|
| ---------------- | ------------------------------------------ |
|
||||||
| `~/.config/xo-cli/` | Global wallet state |
|
| `$XO_CONFIG_DIR` | Global wallet state |
|
||||||
| `./` (cwd) | Templates, invitation JSON, explicit paths |
|
| `./` (cwd) | Templates, invitation JSON, explicit paths |
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
existsSync,
|
existsSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
appendFileSync,
|
appendFileSync,
|
||||||
|
mkdirSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -193,7 +194,7 @@ export function generateFishCompletions(binName: string): string {
|
|||||||
return loadAndProcessTemplate("fish.fish", binName);
|
return loadAndProcessTemplate("fish.fish", binName);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShellType = "bash" | "zsh" | "fish";
|
export type ShellType = "bash" | "zsh" | "fish";
|
||||||
|
|
||||||
const generators: Record<ShellType, (binName: string) => string> = {
|
const generators: Record<ShellType, (binName: string) => string> = {
|
||||||
bash: generateBashCompletions,
|
bash: generateBashCompletions,
|
||||||
@@ -202,51 +203,74 @@ const generators: Record<ShellType, (binName: string) => string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shell config file paths and eval commands for each shell type.
|
* Shell config file paths and startup commands for each shell type.
|
||||||
*/
|
*/
|
||||||
const shellConfigs: Record<
|
const shellConfigs: Record<
|
||||||
ShellType,
|
ShellType,
|
||||||
{ configFile: string; evalCommand: (binName: string) => string }
|
{
|
||||||
|
configFile: string;
|
||||||
|
configDirCommand: string;
|
||||||
|
configDirPattern: RegExp;
|
||||||
|
evalCommand: (binName: string) => string;
|
||||||
|
}
|
||||||
> = {
|
> = {
|
||||||
bash: {
|
bash: {
|
||||||
configFile: join(homedir(), ".bashrc"),
|
configFile: join(homedir(), ".bashrc"),
|
||||||
|
configDirCommand: 'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"',
|
||||||
|
configDirPattern: /^\s*(?:export\s+)?XO_CONFIG_DIR=/m,
|
||||||
evalCommand: (binName) => `eval "$(${binName} completions bash)"`,
|
evalCommand: (binName) => `eval "$(${binName} completions bash)"`,
|
||||||
},
|
},
|
||||||
zsh: {
|
zsh: {
|
||||||
configFile: join(homedir(), ".zshrc"),
|
configFile: join(homedir(), ".zshrc"),
|
||||||
|
configDirCommand: 'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"',
|
||||||
|
configDirPattern: /^\s*(?:export\s+)?XO_CONFIG_DIR=/m,
|
||||||
evalCommand: (binName) => `eval "$(${binName} completions zsh)"`,
|
evalCommand: (binName) => `eval "$(${binName} completions zsh)"`,
|
||||||
},
|
},
|
||||||
fish: {
|
fish: {
|
||||||
configFile: join(homedir(), ".config", "fish", "config.fish"),
|
configFile: join(homedir(), ".config", "fish", "config.fish"),
|
||||||
|
configDirCommand:
|
||||||
|
'set -q XO_CONFIG_DIR; or set -gx XO_CONFIG_DIR "$HOME/.config/xo-cli"',
|
||||||
|
configDirPattern: /^\s*set\b[^\n]*\bXO_CONFIG_DIR\b/m,
|
||||||
evalCommand: (binName) => `${binName} completions fish | source`,
|
evalCommand: (binName) => `${binName} completions fish | source`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs completions to the user's shell config file.
|
* Installs completions to the user's shell config file.
|
||||||
* Adds the eval command if not already present.
|
* Adds a default config directory and the eval command if not already present.
|
||||||
* @param shell - The shell type
|
* @param shell - The shell type
|
||||||
* @param binName - The CLI binary name
|
* @param binName - The CLI binary name
|
||||||
* @returns true if installed, false if already present
|
* @returns true if installed, false if already present
|
||||||
*/
|
*/
|
||||||
function installCompletions(shell: ShellType, binName: string): boolean {
|
export function installCompletions(
|
||||||
const config = shellConfigs[shell];
|
shell: ShellType,
|
||||||
|
binName: string,
|
||||||
|
configFile: string = shellConfigs[shell].configFile,
|
||||||
|
): boolean {
|
||||||
|
const config = { ...shellConfigs[shell], configFile };
|
||||||
const evalCommand = config.evalCommand(binName);
|
const evalCommand = config.evalCommand(binName);
|
||||||
|
|
||||||
// Check if config file exists and already has the completion line
|
|
||||||
let existingContent = "";
|
let existingContent = "";
|
||||||
if (existsSync(config.configFile)) {
|
if (existsSync(config.configFile)) {
|
||||||
existingContent = readFileSync(config.configFile, "utf8");
|
existingContent = readFileSync(config.configFile, "utf8");
|
||||||
if (existingContent.includes(evalCommand)) {
|
|
||||||
return false; // Already installed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the completion line
|
const commands: string[] = [];
|
||||||
|
if (!config.configDirPattern.test(existingContent)) {
|
||||||
|
commands.push(config.configDirCommand);
|
||||||
|
}
|
||||||
|
if (!existingContent.includes(evalCommand)) {
|
||||||
|
commands.push(evalCommand);
|
||||||
|
}
|
||||||
|
if (commands.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const newLine =
|
const newLine =
|
||||||
existingContent.endsWith("\n") || existingContent === "" ? "" : "\n";
|
existingContent.endsWith("\n") || existingContent === "" ? "" : "\n";
|
||||||
const completionBlock = `${newLine}\n# ${binName} shell completions\n${evalCommand}\n`;
|
const completionBlock = `${newLine}\n# ${binName} shell completions\n${commands.join("\n")}\n`;
|
||||||
|
|
||||||
|
mkdirSync(dirname(config.configFile), { recursive: true });
|
||||||
appendFileSync(config.configFile, completionBlock);
|
appendFileSync(config.configFile, completionBlock);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,19 @@ __xo_complete() {
|
|||||||
[[ -n "${__xo_complete_bin}" ]] && "${__xo_complete_bin}" "$@" 2>/dev/null
|
[[ -n "${__xo_complete_bin}" ]] && "${__xo_complete_bin}" "$@" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# @description
|
||||||
|
# Lists mnemonic aliases directly from the config directory without starting
|
||||||
|
# the dynamic Node helper.
|
||||||
|
__xo_complete_mnemonics() {
|
||||||
|
local config_dir="${XO_CONFIG_DIR:-${HOME}/.config/xo-cli}"
|
||||||
|
local file mnemonic
|
||||||
|
for file in "${config_dir}"/mnemonics/mnemonic-*; do
|
||||||
|
[[ -f "${file}" ]] || continue
|
||||||
|
mnemonic="${file##*/}"
|
||||||
|
[[ "${mnemonic}" == "$1"* ]] && printf '%s\n' "${mnemonic}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# @description
|
# @description
|
||||||
# Main completion dispatcher invoked by bash's `complete -F`.
|
# Main completion dispatcher invoked by bash's `complete -F`.
|
||||||
# It determines context (command/subcommand/argument position) and then mixes:
|
# It determines context (command/subcommand/argument position) and then mixes:
|
||||||
@@ -39,10 +52,10 @@ _{{FUNC_NAME}}_completions() {
|
|||||||
_init_completion || return
|
_init_completion || return
|
||||||
|
|
||||||
# If the previous token is `-m/--mnemonic-file`, this argument expects a
|
# If the previous token is `-m/--mnemonic-file`, this argument expects a
|
||||||
# mnemonic file alias/path. Ask the helper for mnemonic suggestions.
|
# mnemonic file alias/path. List mnemonic aliases directly from disk.
|
||||||
if [[ "${prev}" == "-m" || "${prev}" == "--mnemonic-file" ]]; then
|
if [[ "${prev}" == "-m" || "${prev}" == "--mnemonic-file" ]]; then
|
||||||
local mnemonics
|
local mnemonics
|
||||||
mnemonics=$(__xo_complete mnemonics "${cur}")
|
mnemonics=$(__xo_complete_mnemonics "${cur}")
|
||||||
if [[ -n "${mnemonics}" ]]; then
|
if [[ -n "${mnemonics}" ]]; then
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
COMPREPLY+=("$line")
|
COMPREPLY+=("$line")
|
||||||
|
|||||||
@@ -28,6 +28,21 @@ function __{{FUNC_NAME}}_complete_dynamic
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @description
|
||||||
|
# Lists mnemonic aliases directly from the config directory without starting
|
||||||
|
# the dynamic Node helper.
|
||||||
|
function __{{FUNC_NAME}}_complete_mnemonics
|
||||||
|
set -l config_dir "$XO_CONFIG_DIR"
|
||||||
|
if test -z "$config_dir"
|
||||||
|
set config_dir "$HOME/.config/xo-cli"
|
||||||
|
end
|
||||||
|
for file in $config_dir/mnemonics/mnemonic-*
|
||||||
|
if test -f "$file"
|
||||||
|
string replace -r '.*/' '' "$file"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Global option flags available across top-level command contexts.
|
# Global option flags available across top-level command contexts.
|
||||||
complete -c {{BIN_NAME}} -s h -d "Show help"
|
complete -c {{BIN_NAME}} -s h -d "Show help"
|
||||||
complete -c {{BIN_NAME}} -l help -d "Show help"
|
complete -c {{BIN_NAME}} -l help -d "Show help"
|
||||||
@@ -37,8 +52,8 @@ complete -c {{BIN_NAME}} -s o -d "Output file"
|
|||||||
complete -c {{BIN_NAME}} -l output -d "Output file"
|
complete -c {{BIN_NAME}} -l output -d "Output file"
|
||||||
complete -c {{BIN_NAME}} -l currency -d "Set fiat display currency"
|
complete -c {{BIN_NAME}} -l currency -d "Set fiat display currency"
|
||||||
|
|
||||||
# Dynamic completion for `-m/--mnemonic-file`.
|
# Shell-native completion for `-m/--mnemonic-file`.
|
||||||
complete -c {{BIN_NAME}} -s m -l mnemonic-file -xa '(__{{FUNC_NAME}}_complete_dynamic mnemonics)'
|
complete -c {{BIN_NAME}} -s m -l mnemonic-file -xa '(__{{FUNC_NAME}}_complete_mnemonics)'
|
||||||
|
|
||||||
# Top-level command registrations inserted by template expansion.
|
# Top-level command registrations inserted by template expansion.
|
||||||
{{TOP_LEVEL_COMMANDS}}
|
{{TOP_LEVEL_COMMANDS}}
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ __xo_complete() {
|
|||||||
[[ -n "${__xo_complete_bin}" ]] && "${__xo_complete_bin}" "$@" 2>/dev/null
|
[[ -n "${__xo_complete_bin}" ]] && "${__xo_complete_bin}" "$@" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# @description
|
||||||
|
# Lists mnemonic aliases directly from the config directory without starting
|
||||||
|
# the dynamic Node helper.
|
||||||
|
__xo_complete_mnemonics() {
|
||||||
|
local config_dir="${XO_CONFIG_DIR:-${HOME}/.config/xo-cli}"
|
||||||
|
local file mnemonic
|
||||||
|
for file in "${config_dir}"/mnemonics/mnemonic-*(N); do
|
||||||
|
[[ -f "${file}" ]] || continue
|
||||||
|
mnemonic="${file:t}"
|
||||||
|
[[ "${mnemonic}" == "$1"* ]] && print -r -- "${mnemonic}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# @description
|
# @description
|
||||||
# Main zsh completion dispatcher registered via `compdef`.
|
# Main zsh completion dispatcher registered via `compdef`.
|
||||||
# It resolves command context from `$words`/`$CURRENT` and serves:
|
# It resolves command context from `$words`/`$CURRENT` and serves:
|
||||||
@@ -38,7 +51,7 @@ _{{FUNC_NAME}}_completions() {
|
|||||||
# If previous token is `-m/--mnemonic-file`, complete mnemonic sources.
|
# If previous token is `-m/--mnemonic-file`, complete mnemonic sources.
|
||||||
if [[ "${words[CURRENT-1]}" == "-m" || "${words[CURRENT-1]}" == "--mnemonic-file" ]]; then
|
if [[ "${words[CURRENT-1]}" == "-m" || "${words[CURRENT-1]}" == "--mnemonic-file" ]]; then
|
||||||
local mnemonics
|
local mnemonics
|
||||||
mnemonics=("${(@f)$(__xo_complete mnemonics "${words[CURRENT]}")}")
|
mnemonics=("${(@f)$(__xo_complete_mnemonics "${words[CURRENT]}")}")
|
||||||
if [[ ${#mnemonics[@]} -gt 0 ]]; then
|
if [[ ${#mnemonics[@]} -gt 0 ]]; then
|
||||||
compadd -- "${mnemonics[@]}"
|
compadd -- "${mnemonics[@]}"
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const DEFAULT_SETTINGS: SettingsData = {
|
|||||||
/**
|
/**
|
||||||
* Handles loading, migrating, and persisting wallet settings.
|
* Handles loading, migrating, and persisting wallet settings.
|
||||||
*
|
*
|
||||||
* The backing file is `~/.config/xo-cli/.wallet`. Historically it stored a raw
|
* The backing file is `<XO_CONFIG_DIR>/.wallet`. Historically it stored a raw
|
||||||
* mnemonic reference string. This service migrates that legacy format to JSON:
|
* mnemonic reference string. This service migrates that legacy format to JSON:
|
||||||
* `{ "default-mnemonic": "<value>", "currency": "USD" }`.
|
* `{ "default-mnemonic": "<value>", "currency": "USD" }`.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ interface MnemonicFileEntry {
|
|||||||
type FocusSection = 'files' | 'input' | 'saveCheckbox' | 'generateRandomSeed' | 'button';
|
type FocusSection = 'files' | 'input' | 'saveCheckbox' | 'generateRandomSeed' | 'button';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads mnemonic-* files from ~/.config/xo-cli/mnemonics/ (same as xo-cli),
|
* Reads mnemonic-* files from the configured mnemonics directory (same as xo-cli),
|
||||||
* then from cwd for legacy installs. Parses each as a BCHMnemonicURL.
|
* then from cwd for legacy installs. Parses each as a BCHMnemonicURL.
|
||||||
*/
|
*/
|
||||||
function loadMnemonicFiles(): MnemonicFileEntry[] {
|
function loadMnemonicFiles(): MnemonicFileEntry[] {
|
||||||
@@ -101,7 +101,7 @@ export function SeedInputScreen(): React.ReactElement {
|
|||||||
const [mnemonicFiles, setMnemonicFiles] = useState<MnemonicFileEntry[]>([]);
|
const [mnemonicFiles, setMnemonicFiles] = useState<MnemonicFileEntry[]>([]);
|
||||||
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
|
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
|
||||||
|
|
||||||
/** When set, manual seed is written to ~/.config/xo-cli/mnemonics/ after a successful unlock. */
|
/** When set, manual seed is written to the configured mnemonics directory after a successful unlock. */
|
||||||
const [saveMnemonicChecked, setSaveMnemonicChecked] = useState(false);
|
const [saveMnemonicChecked, setSaveMnemonicChecked] = useState(false);
|
||||||
|
|
||||||
// Focus: when saved wallets exist default to the file list, otherwise the input.
|
// Focus: when saved wallets exist default to the file list, otherwise the input.
|
||||||
@@ -397,7 +397,7 @@ export function SeedInputScreen(): React.ReactElement {
|
|||||||
{saveMnemonicChecked ? '[x] ' : '[ ] '}
|
{saveMnemonicChecked ? '[x] ' : '[ ] '}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={colors.text}>Save this mnemonic</Text>
|
<Text color={colors.text}>Save this mnemonic</Text>
|
||||||
<Text color={colors.textMuted}> (~/.config/xo-cli/mnemonics/)</Text>
|
<Text color={colors.textMuted}> ({getMnemonicsDir()}/)</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{focusedSection === 'saveCheckbox' && (
|
{focusedSection === 'saveCheckbox' && (
|
||||||
<Box marginTop={0} paddingX={1}>
|
<Box marginTop={0} paddingX={1}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Global XO CLI config layout (XDG-style: ~/.config/xo-cli/).
|
* Global XO CLI config layout (`XO_CONFIG_DIR` or ~/.config/xo-cli/).
|
||||||
* User-provided paths (templates, invitation JSON) stay relative to cwd.
|
* User-provided paths (templates, invitation JSON) stay relative to cwd.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ import { basename, isAbsolute, join, resolve } from "node:path";
|
|||||||
* Base config directory. Created on first access.
|
* Base config directory. Created on first access.
|
||||||
*/
|
*/
|
||||||
export function getConfigDir(): string {
|
export function getConfigDir(): string {
|
||||||
const dir = join(homedir(), ".config", "xo-cli");
|
const dir = process.env["XO_CONFIG_DIR"] || join(homedir(), ".config", "xo-cli");
|
||||||
mkdirSync(dir, { recursive: true });
|
mkdirSync(dir, { recursive: true });
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ export function getWalletConfigPath(): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a mnemonic reference to an absolute path.
|
* Resolves a mnemonic reference to an absolute path.
|
||||||
* Order: absolute path if it exists → path relative to cwd → ~/.config/xo-cli/mnemonics/<basename>.
|
* Order: absolute path if it exists → path relative to cwd → config mnemonics directory/<basename>.
|
||||||
*
|
*
|
||||||
* @param mnemonicRef - Path or basename (e.g. `mnemonic-nuclear`)
|
* @param mnemonicRef - Path or basename (e.g. `mnemonic-nuclear`)
|
||||||
* @returns Absolute path to the mnemonic file
|
* @returns Absolute path to the mnemonic file
|
||||||
|
|||||||
112
tests/cli/autocomplete-completions.test.ts
Normal file
112
tests/cli/autocomplete-completions.test.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "vitest";
|
||||||
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateBashCompletions,
|
||||||
|
generateFishCompletions,
|
||||||
|
generateZshCompletions,
|
||||||
|
installCompletions,
|
||||||
|
} from "../../src/cli/autocomplete/completions";
|
||||||
|
|
||||||
|
describe("shell completions", () => {
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const tempDir of tempDirs) {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
tempDirs.length = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
function createConfigFile(contents = ""): string {
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), "xo-cli-completions-test-"));
|
||||||
|
tempDirs.push(tempDir);
|
||||||
|
const configFile = join(tempDir, "shellrc");
|
||||||
|
writeFileSync(configFile, contents);
|
||||||
|
return configFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("uses shell-native mnemonic completion in bash", () => {
|
||||||
|
const completions = generateBashCompletions("xo-cli");
|
||||||
|
|
||||||
|
expect(completions).toContain(
|
||||||
|
'local config_dir="${XO_CONFIG_DIR:-${HOME}/.config/xo-cli}"',
|
||||||
|
);
|
||||||
|
expect(completions).toContain('__xo_complete_mnemonics "${cur}"');
|
||||||
|
expect(completions).not.toContain('__xo_complete mnemonics "${cur}"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses shell-native mnemonic completion in zsh", () => {
|
||||||
|
const completions = generateZshCompletions("xo-cli");
|
||||||
|
|
||||||
|
expect(completions).toContain(
|
||||||
|
'local config_dir="${XO_CONFIG_DIR:-${HOME}/.config/xo-cli}"',
|
||||||
|
);
|
||||||
|
expect(completions).toContain(
|
||||||
|
'__xo_complete_mnemonics "${words[CURRENT]}"',
|
||||||
|
);
|
||||||
|
expect(completions).not.toContain(
|
||||||
|
'__xo_complete mnemonics "${words[CURRENT]}"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses shell-native mnemonic completion in fish", () => {
|
||||||
|
const completions = generateFishCompletions("xo-cli");
|
||||||
|
|
||||||
|
expect(completions).toContain("set -l config_dir \"$XO_CONFIG_DIR\"");
|
||||||
|
expect(completions).toContain("(__xo_cli_complete_mnemonics)");
|
||||||
|
expect(completions).not.toContain("(__xo_cli_complete_dynamic mnemonics)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("installs the config default and completion loader once", () => {
|
||||||
|
const configFile = createConfigFile();
|
||||||
|
|
||||||
|
expect(installCompletions("bash", "xo-cli", configFile)).toBe(true);
|
||||||
|
expect(installCompletions("bash", "xo-cli", configFile)).toBe(false);
|
||||||
|
|
||||||
|
const contents = readFileSync(configFile, "utf8");
|
||||||
|
expect(contents.match(/XO_CONFIG_DIR/g)).toHaveLength(2);
|
||||||
|
expect(contents.match(/eval "\$\(xo-cli completions bash\)"/g)).toHaveLength(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adds a missing default without duplicating an existing loader", () => {
|
||||||
|
const configFile = createConfigFile('eval "$(xo-cli completions bash)"\n');
|
||||||
|
|
||||||
|
expect(installCompletions("bash", "xo-cli", configFile)).toBe(true);
|
||||||
|
|
||||||
|
const contents = readFileSync(configFile, "utf8");
|
||||||
|
expect(contents.match(/eval "\$\(xo-cli completions bash\)"/g)).toHaveLength(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
expect(contents).toContain(
|
||||||
|
'export XO_CONFIG_DIR="${XO_CONFIG_DIR:-$HOME/.config/xo-cli}"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves an existing custom config directory assignment", () => {
|
||||||
|
const configFile = createConfigFile("export XO_CONFIG_DIR=/tmp/custom-xo\n");
|
||||||
|
|
||||||
|
expect(installCompletions("zsh", "xo-cli", configFile)).toBe(true);
|
||||||
|
|
||||||
|
const contents = readFileSync(configFile, "utf8");
|
||||||
|
expect(contents).toContain("export XO_CONFIG_DIR=/tmp/custom-xo");
|
||||||
|
expect(contents).not.toContain("${XO_CONFIG_DIR:-$HOME/.config/xo-cli}");
|
||||||
|
expect(contents).toContain('eval "$(xo-cli completions zsh)"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses fish syntax when installing fish completions", () => {
|
||||||
|
const configFile = createConfigFile();
|
||||||
|
|
||||||
|
expect(installCompletions("fish", "xo-cli", configFile)).toBe(true);
|
||||||
|
|
||||||
|
const contents = readFileSync(configFile, "utf8");
|
||||||
|
expect(contents).toContain(
|
||||||
|
'set -q XO_CONFIG_DIR; or set -gx XO_CONFIG_DIR "$HOME/.config/xo-cli"',
|
||||||
|
);
|
||||||
|
expect(contents).toContain("xo-cli completions fish | source");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,20 @@ import {
|
|||||||
} from "../../src/utils/paths";
|
} from "../../src/utils/paths";
|
||||||
|
|
||||||
describe("paths utilities", () => {
|
describe("paths utilities", () => {
|
||||||
|
const originalConfigDir = process.env["XO_CONFIG_DIR"];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env["XO_CONFIG_DIR"];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalConfigDir === undefined) {
|
||||||
|
delete process.env["XO_CONFIG_DIR"];
|
||||||
|
} else {
|
||||||
|
process.env["XO_CONFIG_DIR"] = originalConfigDir;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe("getConfigDir", () => {
|
describe("getConfigDir", () => {
|
||||||
test("returns path under ~/.config/xo-cli", () => {
|
test("returns path under ~/.config/xo-cli", () => {
|
||||||
const configDir = getConfigDir();
|
const configDir = getConfigDir();
|
||||||
@@ -24,6 +38,26 @@ describe("paths utilities", () => {
|
|||||||
|
|
||||||
expect(existsSync(configDir)).toBe(true);
|
expect(existsSync(configDir)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("uses XO_CONFIG_DIR when configured", () => {
|
||||||
|
const customDir = path.join(tmpdir(), `xo-cli-config-test-${Date.now()}`);
|
||||||
|
process.env["XO_CONFIG_DIR"] = customDir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(getConfigDir()).toBe(customDir);
|
||||||
|
expect(getMnemonicsDir()).toBe(path.join(customDir, "mnemonics"));
|
||||||
|
expect(getDataDir()).toBe(path.join(customDir, "data"));
|
||||||
|
expect(getWalletConfigPath()).toBe(path.join(customDir, ".wallet"));
|
||||||
|
} finally {
|
||||||
|
rmSync(customDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses the default when XO_CONFIG_DIR is empty", () => {
|
||||||
|
process.env["XO_CONFIG_DIR"] = "";
|
||||||
|
|
||||||
|
expect(getConfigDir()).toBe(path.join(homedir(), ".config", "xo-cli"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getMnemonicsDir", () => {
|
describe("getMnemonicsDir", () => {
|
||||||
@@ -106,6 +140,7 @@ describe("paths utilities", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("resolves from global mnemonics dir when file exists there", () => {
|
test("resolves from global mnemonics dir when file exists there", () => {
|
||||||
|
process.env["XO_CONFIG_DIR"] = tempDir;
|
||||||
const mnemonicsDir = getMnemonicsDir();
|
const mnemonicsDir = getMnemonicsDir();
|
||||||
const testFile = path.join(mnemonicsDir, "mnemonic-global-test");
|
const testFile = path.join(mnemonicsDir, "mnemonic-global-test");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user