@1sat/cli 0.0.76 → 0.0.78

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.
package/README.md CHANGED
@@ -72,6 +72,12 @@ export PRIVATE_KEY_WIF="<your WIF key>"
72
72
  1sat wallet balance
73
73
  ```
74
74
 
75
+ **Env file** (same keys as above; file values override the process environment for this run):
76
+
77
+ ```bash
78
+ 1sat --env-file mint.env wallet balance
79
+ ```
80
+
75
81
  **Encrypted keyfile (interactive use):**
76
82
 
77
83
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.76",
3
+ "version": "0.0.78",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
@@ -26,12 +26,12 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@1sat/actions": "0.0.174",
29
+ "@1sat/actions": "0.0.184",
30
30
  "@1sat/client": "0.0.42",
31
- "@1sat/types": "0.0.33",
31
+ "@1sat/types": "0.0.34",
32
32
  "@1sat/wallet-node": "0.0.57",
33
33
  "@1sat/wallet-server": "0.0.26",
34
- "@bsv/sdk": "^2.0.13",
34
+ "@bsv/sdk": "^2.1.6",
35
35
  "@bsv/wallet-toolbox": "2.1.24",
36
36
  "chalk": "^5.0.0",
37
37
  "@clack/prompts": "^0.8.0",
package/src/args.ts CHANGED
@@ -9,6 +9,8 @@ export interface GlobalFlags {
9
9
  quiet: boolean
10
10
  yes: boolean
11
11
  chain: 'main' | 'test'
12
+ /** Paths from `--env-file` / `--env-file=path` (may be empty). */
13
+ envFiles: string[]
12
14
  help: boolean
13
15
  version: boolean
14
16
  rest: string[]
@@ -24,6 +26,7 @@ export function parseGlobalFlags(args: string[]): GlobalFlags {
24
26
  let chain: 'main' | 'test' = 'main'
25
27
  let help = false
26
28
  let version = false
29
+ const envFiles: string[] = []
27
30
  const rest: string[] = []
28
31
  const skip = new Set<number>()
29
32
 
@@ -56,6 +59,15 @@ export function parseGlobalFlags(args: string[]): GlobalFlags {
56
59
  }
57
60
  break
58
61
  }
62
+ case '--env-file': {
63
+ const value = args[i + 1]
64
+ if (!value || value.startsWith('-')) {
65
+ throw new Error('--env-file requires a path')
66
+ }
67
+ envFiles.push(value)
68
+ skip.add(i + 1)
69
+ break
70
+ }
59
71
  case '--help':
60
72
  case '-h':
61
73
  help = true
@@ -65,11 +77,19 @@ export function parseGlobalFlags(args: string[]): GlobalFlags {
65
77
  version = true
66
78
  break
67
79
  default:
68
- rest.push(arg)
80
+ if (arg.startsWith('--env-file=')) {
81
+ const value = arg.slice('--env-file='.length)
82
+ if (!value) {
83
+ throw new Error('--env-file requires a path')
84
+ }
85
+ envFiles.push(value)
86
+ } else {
87
+ rest.push(arg)
88
+ }
69
89
  }
70
90
  }
71
91
 
72
- return { json, quiet, yes, chain, help, version, rest }
92
+ return { json, quiet, yes, chain, envFiles, help, version, rest }
73
93
  }
74
94
 
75
95
  /**
package/src/cli.ts CHANGED
@@ -1,130 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  /**
4
- * 1sat CLI - Command-line interface for 1Sat Ordinals SDK.
5
- *
6
- * Pure Bun CLI with manual arg parsing. No frameworks.
4
+ * CLI entry. Must set DOTENV_CONFIG_QUIET before any import that pulls in
5
+ * @bsv/wallet-toolbox (Setup / MonitorDaemon call dotenv.config() at load).
7
6
  */
8
7
 
9
- import { parseGlobalFlags } from './args'
10
- import { handleActionCommand } from './commands/action'
11
- import { handleConfigCommand } from './commands/config'
12
- import { handleIdentityCommand } from './commands/identity'
13
- import { handleInitCommand } from './commands/init'
14
- import { handleLocksCommand } from './commands/locks'
15
- import { handleMcpProxyCommand } from './commands/mcp-proxy'
16
- import { handleOpnsCommand } from './commands/opns'
17
- import { handleOrdinalsCommand } from './commands/ordinals'
18
- import { handleRemoteCommand } from './commands/remote'
19
- import { handleServeCommand } from './commands/serve'
20
- import { handleSocialCommand } from './commands/social'
21
- import { handleSweepCommand } from './commands/sweep'
22
- import { handleTokensCommand } from './commands/tokens'
23
- import { handleTxCommand } from './commands/tx'
24
- import { handleWalletCommand } from './commands/wallet'
25
- import { getCommand, printCommandHelp, printHelp, printVersion } from './help'
26
- import { formatError } from './output'
8
+ process.env.DOTENV_CONFIG_QUIET = 'true'
27
9
 
28
- const rawArgs = process.argv.slice(2)
29
-
30
- async function main(): Promise<void> {
31
- const flags = parseGlobalFlags(rawArgs)
32
-
33
- if (flags.version) {
34
- printVersion()
35
- process.exit(0)
36
- }
37
-
38
- const [command, ...rest] = flags.rest
39
-
40
- if (!command) {
41
- printHelp(flags.json)
42
- process.exit(0)
43
- }
44
-
45
- if (flags.help) {
46
- if (getCommand(command)) {
47
- printCommandHelp(command, flags.json)
48
- } else {
49
- printHelp(flags.json)
50
- }
51
- process.exit(0)
52
- }
53
-
54
- switch (command) {
55
- case 'init':
56
- await handleInitCommand(rest, flags)
57
- break
58
-
59
- case 'config':
60
- await handleConfigCommand(rest, flags)
61
- break
62
-
63
- case 'remote':
64
- await handleRemoteCommand(rest, flags)
65
- break
66
-
67
- case 'wallet':
68
- await handleWalletCommand(rest, flags)
69
- break
70
-
71
- case 'ordinals':
72
- await handleOrdinalsCommand(rest, flags)
73
- break
74
-
75
- case 'tokens':
76
- await handleTokensCommand(rest, flags)
77
- break
78
-
79
- case 'locks':
80
- await handleLocksCommand(rest, flags)
81
- break
82
-
83
- case 'identity':
84
- await handleIdentityCommand(rest, flags)
85
- break
86
-
87
- case 'social':
88
- await handleSocialCommand(rest, flags)
89
- break
90
-
91
- case 'opns':
92
- await handleOpnsCommand(rest, flags)
93
- break
94
-
95
- case 'sweep':
96
- await handleSweepCommand(rest, flags)
97
- break
98
-
99
- case 'action':
100
- await handleActionCommand(rest, flags)
101
- break
102
-
103
- case 'tx':
104
- await handleTxCommand(rest, flags)
105
- break
106
-
107
- case 'mcp-proxy':
108
- await handleMcpProxyCommand()
109
- break
110
-
111
- case 'serve':
112
- await handleServeCommand(rest, flags)
113
- break
114
-
115
- case 'help':
116
- printHelp(flags.json)
117
- break
118
-
119
- default:
120
- console.error(formatError(`Unknown command: ${command}`))
121
- printHelp(flags.json)
122
- process.exit(1)
123
- }
124
- }
125
-
126
- main().catch((err) => {
127
- console.error(formatError(`Error: ${err.message}`))
128
- if (process.env.DEBUG) console.error(err.stack)
129
- process.exit(1)
130
- })
10
+ await import('./main.ts')
@@ -325,7 +325,7 @@ async function runWithStorage(
325
325
  async stop() {
326
326
  if (mode !== 'wallet') {
327
327
  walletResult.monitor.stopTasks()
328
- clearMonitorPid(resolved.dataDir)
328
+ clearMonitorPid(resolved.dataDir, process.pid)
329
329
  }
330
330
  if (serverHandle) await serverHandle.stop()
331
331
  // Accounts shares the wallet's connection — walletResult.destroy
package/src/context.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * OneSatContext factory for CLI commands.
3
3
  *
4
- * Creates a fully initialized wallet context with services and monitor.
4
+ * Creates a fully initialized wallet context with services. Monitor work is
5
+ * not run in-process (avoids toolbox/console noise on the TTY). After the
6
+ * wallet is destroyed, a detached `__monitor-once` child may run against the
7
+ * same storage with logs in <dataDir>/monitor.log.
5
8
  */
6
9
 
7
10
  import { type OneSatContext, createContext } from '@1sat/actions'
8
11
  import { type NodeWalletResult, createNodeWallet } from '@1sat/wallet-node'
9
12
  import type { PrivateKey } from '@bsv/sdk'
10
13
  import { ensureDataDir, loadConfig } from './config'
11
- import { readLiveMonitorPid } from './monitor-lock'
14
+ import { spawnDetachedMonitorOnce } from './monitor-once'
12
15
 
13
16
  /** Extended context that includes cleanup */
14
17
  export interface CliContext {
@@ -23,10 +26,8 @@ export interface CliContext {
23
26
  * Sets up:
24
27
  * - Node wallet with SQLite storage
25
28
  * - 1Sat services for API access
26
- * - Monitor for transaction lifecycle. When local storage is the active
27
- * store, the wallet factory fires `monitor.runOnce()` internally on
28
- * creation; individual tasks self-throttle via their own intervals, so
29
- * repeated CLI invocations are cheap.
29
+ * - No in-process monitor.runOnce(); a background once-run is scheduled on
30
+ * destroy when local storage is active and no other monitor owner is live.
30
31
  */
31
32
  export async function loadContext(
32
33
  privateKey: PrivateKey,
@@ -37,8 +38,6 @@ export async function loadContext(
37
38
 
38
39
  const storageIdentityKey = config.storageIdentityKey ?? '1sat-cli-default'
39
40
 
40
- const skipInitialMonitor = readLiveMonitorPid(dataDir) !== undefined
41
-
42
41
  const walletResult = await createNodeWallet({
43
42
  privateKey,
44
43
  chain: opts.chain,
@@ -49,7 +48,8 @@ export async function loadContext(
49
48
  },
50
49
  activeRemote: config.activeRemote,
51
50
  backups: config.backups,
52
- skipInitialMonitor,
51
+ // Monitor runs in a detached child after destroy, or in `1sat serve`.
52
+ skipInitialMonitor: true,
53
53
  })
54
54
 
55
55
  const ctx = createContext(walletResult.wallet, {
@@ -61,6 +61,14 @@ export async function loadContext(
61
61
  return {
62
62
  ctx,
63
63
  walletResult,
64
- destroy: walletResult.destroy,
64
+ destroy: async () => {
65
+ await walletResult.destroy()
66
+ spawnDetachedMonitorOnce({
67
+ dataDir,
68
+ chain: opts.chain,
69
+ privateKeyWif: privateKey.toWif(),
70
+ activeRemote: config.activeRemote,
71
+ })
72
+ },
65
73
  }
66
74
  }
package/src/help.ts CHANGED
@@ -62,6 +62,12 @@ export const GLOBAL_OPTIONS: ArgSpec[] = [
62
62
  values: '<main|test>',
63
63
  description: 'Network (default: main)',
64
64
  },
65
+ {
66
+ flag: '--env-file',
67
+ values: '<path>',
68
+ description:
69
+ 'Load env vars from a file (repeatable; file values override existing env)',
70
+ },
65
71
  { flag: '--help', description: 'Show help (also -h)' },
66
72
  { flag: '--version', description: 'Show version (also -v)' },
67
73
  ]
package/src/main.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * 1sat CLI - Command-line interface for 1Sat Ordinals SDK.
3
+ *
4
+ * Pure Bun CLI with manual arg parsing. No frameworks.
5
+ *
6
+ * Loaded after entry bootstrap sets DOTENV_CONFIG_QUIET so wallet-toolbox
7
+ * import-time dotenv.config() calls stay silent.
8
+ */
9
+
10
+ import { existsSync } from 'node:fs'
11
+ import { resolve } from 'node:path'
12
+ import { config as loadEnv } from 'dotenv'
13
+ import { parseGlobalFlags } from './args'
14
+ import { handleActionCommand } from './commands/action'
15
+ import { handleConfigCommand } from './commands/config'
16
+ import { handleIdentityCommand } from './commands/identity'
17
+ import { handleInitCommand } from './commands/init'
18
+ import { handleLocksCommand } from './commands/locks'
19
+ import { handleMcpProxyCommand } from './commands/mcp-proxy'
20
+ import { handleOpnsCommand } from './commands/opns'
21
+ import { handleOrdinalsCommand } from './commands/ordinals'
22
+ import { handleRemoteCommand } from './commands/remote'
23
+ import { handleServeCommand } from './commands/serve'
24
+ import { handleSocialCommand } from './commands/social'
25
+ import { handleSweepCommand } from './commands/sweep'
26
+ import { handleTokensCommand } from './commands/tokens'
27
+ import { handleTxCommand } from './commands/tx'
28
+ import { handleWalletCommand } from './commands/wallet'
29
+ import { getCommand, printCommandHelp, printHelp, printVersion } from './help'
30
+ import { runMonitorOnce } from './monitor-once'
31
+ import { formatError } from './output'
32
+
33
+ const rawArgs = process.argv.slice(2)
34
+
35
+ /** Load `--env-file` paths into process.env. File values win over existing env. */
36
+ function applyEnvFiles(paths: string[]): void {
37
+ for (const p of paths) {
38
+ const abs = resolve(p)
39
+ if (!existsSync(abs)) {
40
+ throw new Error(`Env file not found: ${p}`)
41
+ }
42
+ const result = loadEnv({ path: abs, quiet: true, override: true })
43
+ if (result.error) {
44
+ throw result.error
45
+ }
46
+ }
47
+ }
48
+
49
+ async function main(): Promise<void> {
50
+ const flags = parseGlobalFlags(rawArgs)
51
+ if (flags.envFiles.length > 0) {
52
+ applyEnvFiles(flags.envFiles)
53
+ }
54
+
55
+ if (flags.version) {
56
+ printVersion()
57
+ process.exit(0)
58
+ }
59
+
60
+ const [command, ...rest] = flags.rest
61
+
62
+ if (!command) {
63
+ printHelp(flags.json)
64
+ process.exit(0)
65
+ }
66
+
67
+ if (flags.help) {
68
+ if (getCommand(command)) {
69
+ printCommandHelp(command, flags.json)
70
+ } else {
71
+ printHelp(flags.json)
72
+ }
73
+ process.exit(0)
74
+ }
75
+
76
+ switch (command) {
77
+ case 'init':
78
+ await handleInitCommand(rest, flags)
79
+ break
80
+
81
+ case 'config':
82
+ await handleConfigCommand(rest, flags)
83
+ break
84
+
85
+ case 'remote':
86
+ await handleRemoteCommand(rest, flags)
87
+ break
88
+
89
+ case 'wallet':
90
+ await handleWalletCommand(rest, flags)
91
+ break
92
+
93
+ case 'ordinals':
94
+ await handleOrdinalsCommand(rest, flags)
95
+ break
96
+
97
+ case 'tokens':
98
+ await handleTokensCommand(rest, flags)
99
+ break
100
+
101
+ case 'locks':
102
+ await handleLocksCommand(rest, flags)
103
+ break
104
+
105
+ case 'identity':
106
+ await handleIdentityCommand(rest, flags)
107
+ break
108
+
109
+ case 'social':
110
+ await handleSocialCommand(rest, flags)
111
+ break
112
+
113
+ case 'opns':
114
+ await handleOpnsCommand(rest, flags)
115
+ break
116
+
117
+ case 'sweep':
118
+ await handleSweepCommand(rest, flags)
119
+ break
120
+
121
+ case 'action':
122
+ await handleActionCommand(rest, flags)
123
+ break
124
+
125
+ case 'tx':
126
+ await handleTxCommand(rest, flags)
127
+ break
128
+
129
+ case 'mcp-proxy':
130
+ await handleMcpProxyCommand()
131
+ break
132
+
133
+ case 'serve':
134
+ await handleServeCommand(rest, flags)
135
+ break
136
+
137
+ // Hidden: parent CLI spawns this after wallet destroy so monitor
138
+ // stdout/stderr go to ~/.1sat/cli/monitor.log instead of the TTY.
139
+ case '__monitor-once':
140
+ await runMonitorOnce(flags.chain)
141
+ break
142
+
143
+ case 'help':
144
+ printHelp(flags.json)
145
+ break
146
+
147
+ default:
148
+ console.error(formatError(`Unknown command: ${command}`))
149
+ printHelp(flags.json)
150
+ process.exit(1)
151
+ }
152
+ }
153
+
154
+ main().catch((err) => {
155
+ console.error(formatError(`Error: ${err.message}`))
156
+ if (process.env.DEBUG) console.error(err.stack)
157
+ process.exit(1)
158
+ })
@@ -3,10 +3,9 @@
3
3
  * and a long-running `1sat serve` process.
4
4
  *
5
5
  * - `1sat serve` (modes that run the monitor) writes the pid on startup and
6
- * removes it on clean shutdown.
7
- * - CLI invocations read the file and, if the pid is alive, skip firing
8
- * their own `monitor.runOnce()` to avoid duplicate work against the same
9
- * SQLite file.
6
+ * removes it on clean shutdown (only if still the owner).
7
+ * - Detached `__monitor-once` children claim the same lock while they run.
8
+ * - CLI parents skip spawning another once-run when a live owner is present.
10
9
  */
11
10
 
12
11
  import { readFileSync, unlinkSync, writeFileSync } from 'node:fs'
@@ -25,9 +24,23 @@ export function writeMonitorPid(
25
24
  writeFileSync(monitorPidPath(dataDir), `${pid}\n`, 'utf8')
26
25
  }
27
26
 
28
- export function clearMonitorPid(dataDir: string): void {
27
+ /**
28
+ * Remove the pid file. When `onlyPid` is set, only unlink if the file still
29
+ * contains that pid (avoids a short once-run clearing a serve owner that
30
+ * started while the once-run was shutting down).
31
+ */
32
+ export function clearMonitorPid(
33
+ dataDir: string,
34
+ onlyPid?: number,
35
+ ): void {
36
+ const path = monitorPidPath(dataDir)
29
37
  try {
30
- unlinkSync(monitorPidPath(dataDir))
38
+ if (onlyPid !== undefined) {
39
+ const raw = readFileSync(path, 'utf8')
40
+ const pid = Number.parseInt(raw.trim(), 10)
41
+ if (pid !== onlyPid) return
42
+ }
43
+ unlinkSync(path)
31
44
  } catch {}
32
45
  }
33
46
 
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Detached one-shot monitor run for CLI commands.
3
+ *
4
+ * Parent commands skip in-process monitor.runOnce() so toolbox/console noise
5
+ * never hits the TTY. After the wallet is closed they spawn this process;
6
+ * its stdout/stderr append to <dataDir>/monitor.log.
7
+ */
8
+
9
+ import { type SpawnOptions, spawn } from 'node:child_process'
10
+ import { appendFileSync, closeSync, openSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+ import { createNodeWallet } from '@1sat/wallet-node'
13
+ import { ensureDataDir, loadConfig } from './config'
14
+ import { loadKey } from './keys'
15
+ import {
16
+ clearMonitorPid,
17
+ readLiveMonitorPid,
18
+ writeMonitorPid,
19
+ } from './monitor-lock'
20
+
21
+ export const MONITOR_LOG_FILENAME = 'monitor.log'
22
+
23
+ export function monitorLogPath(dataDir: string): string {
24
+ return join(dataDir, MONITOR_LOG_FILENAME)
25
+ }
26
+
27
+ /**
28
+ * Spawn a detached `__monitor-once` child. No-op when a live monitor owner
29
+ * exists (serve or another once-run) or when a remote is the active store.
30
+ * Spawn failures are reported on the parent stderr; child logs go to file.
31
+ */
32
+ export function spawnDetachedMonitorOnce(opts: {
33
+ dataDir: string
34
+ chain: 'main' | 'test'
35
+ privateKeyWif: string
36
+ activeRemote?: string
37
+ }): void {
38
+ if (opts.activeRemote) return
39
+ if (readLiveMonitorPid(opts.dataDir) !== undefined) return
40
+
41
+ const logPath = monitorLogPath(opts.dataDir)
42
+ try {
43
+ appendFileSync(
44
+ logPath,
45
+ `\n--- monitor-once ${new Date().toISOString()} ---\n`,
46
+ )
47
+ } catch {
48
+ // still try to spawn; openSync may surface the real error
49
+ }
50
+
51
+ let logFd: number
52
+ try {
53
+ logFd = openSync(logPath, 'a')
54
+ } catch (err) {
55
+ console.error(
56
+ `[monitor] failed to open log ${logPath}: ${(err as Error).message}`,
57
+ )
58
+ return
59
+ }
60
+
61
+ const args = ['__monitor-once', '--chain', opts.chain]
62
+ const env: NodeJS.ProcessEnv = {
63
+ ...process.env,
64
+ DOTENV_CONFIG_QUIET: 'true',
65
+ PRIVATE_KEY_WIF: opts.privateKeyWif,
66
+ }
67
+
68
+ try {
69
+ const child = spawnSelf(args, {
70
+ detached: true,
71
+ stdio: ['ignore', logFd, logFd],
72
+ env,
73
+ })
74
+ child.unref()
75
+ } catch (err) {
76
+ console.error(
77
+ `[monitor] failed to start background run: ${(err as Error).message}`,
78
+ )
79
+ } finally {
80
+ closeSync(logFd)
81
+ }
82
+ }
83
+
84
+ function spawnSelf(args: string[], options: SpawnOptions) {
85
+ const script = process.argv[1]
86
+ if (
87
+ script &&
88
+ (script.endsWith('.ts') ||
89
+ script.endsWith('.tsx') ||
90
+ script.endsWith('.js') ||
91
+ script.endsWith('.mjs') ||
92
+ script.endsWith('.cjs'))
93
+ ) {
94
+ return spawn(process.execPath, [script, ...args], options)
95
+ }
96
+ return spawn(process.execPath, args, options)
97
+ }
98
+
99
+ /**
100
+ * Child entry: open the wallet, run monitor once, exit. Claims monitor.pid
101
+ * while running so concurrent CLI spawns skip; clears only if we still own it.
102
+ */
103
+ export async function runMonitorOnce(chain: 'main' | 'test'): Promise<void> {
104
+ const config = loadConfig()
105
+ if (config.activeRemote) return
106
+
107
+ const dataDir = ensureDataDir()
108
+ const existing = readLiveMonitorPid(dataDir)
109
+ if (existing !== undefined && existing !== process.pid) return
110
+
111
+ const privateKey = await loadKey()
112
+ writeMonitorPid(dataDir)
113
+ try {
114
+ const storageIdentityKey = config.storageIdentityKey ?? '1sat-cli-default'
115
+ const walletResult = await createNodeWallet({
116
+ privateKey,
117
+ chain,
118
+ storageIdentityKey,
119
+ storage: {
120
+ provider: 'bun-sqlite',
121
+ filename: `${dataDir}/wallet-${chain}.db`,
122
+ },
123
+ activeRemote: config.activeRemote,
124
+ backups: config.backups,
125
+ skipInitialMonitor: true,
126
+ })
127
+ try {
128
+ await walletResult.monitor.runOnce()
129
+ } finally {
130
+ await walletResult.destroy()
131
+ }
132
+ } finally {
133
+ clearMonitorPid(dataDir, process.pid)
134
+ }
135
+ }