@1sat/cli 0.0.77 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.77",
3
+ "version": "0.0.78",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
package/src/cli.ts CHANGED
@@ -1,150 +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 { existsSync } from 'node:fs'
10
- import { resolve } from 'node:path'
11
- import { config as loadEnv } from 'dotenv'
12
- import { parseGlobalFlags } from './args'
13
- import { handleActionCommand } from './commands/action'
14
- import { handleConfigCommand } from './commands/config'
15
- import { handleIdentityCommand } from './commands/identity'
16
- import { handleInitCommand } from './commands/init'
17
- import { handleLocksCommand } from './commands/locks'
18
- import { handleMcpProxyCommand } from './commands/mcp-proxy'
19
- import { handleOpnsCommand } from './commands/opns'
20
- import { handleOrdinalsCommand } from './commands/ordinals'
21
- import { handleRemoteCommand } from './commands/remote'
22
- import { handleServeCommand } from './commands/serve'
23
- import { handleSocialCommand } from './commands/social'
24
- import { handleSweepCommand } from './commands/sweep'
25
- import { handleTokensCommand } from './commands/tokens'
26
- import { handleTxCommand } from './commands/tx'
27
- import { handleWalletCommand } from './commands/wallet'
28
- import { getCommand, printCommandHelp, printHelp, printVersion } from './help'
29
- import { formatError } from './output'
8
+ process.env.DOTENV_CONFIG_QUIET = 'true'
30
9
 
31
- const rawArgs = process.argv.slice(2)
32
-
33
- /** Load `--env-file` paths into process.env. File values win over existing env. */
34
- function applyEnvFiles(paths: string[]): void {
35
- for (const p of paths) {
36
- const abs = resolve(p)
37
- if (!existsSync(abs)) {
38
- throw new Error(`Env file not found: ${p}`)
39
- }
40
- const result = loadEnv({ path: abs, quiet: true, override: true })
41
- if (result.error) {
42
- throw result.error
43
- }
44
- }
45
- }
46
-
47
- async function main(): Promise<void> {
48
- const flags = parseGlobalFlags(rawArgs)
49
- if (flags.envFiles.length > 0) {
50
- applyEnvFiles(flags.envFiles)
51
- }
52
-
53
- if (flags.version) {
54
- printVersion()
55
- process.exit(0)
56
- }
57
-
58
- const [command, ...rest] = flags.rest
59
-
60
- if (!command) {
61
- printHelp(flags.json)
62
- process.exit(0)
63
- }
64
-
65
- if (flags.help) {
66
- if (getCommand(command)) {
67
- printCommandHelp(command, flags.json)
68
- } else {
69
- printHelp(flags.json)
70
- }
71
- process.exit(0)
72
- }
73
-
74
- switch (command) {
75
- case 'init':
76
- await handleInitCommand(rest, flags)
77
- break
78
-
79
- case 'config':
80
- await handleConfigCommand(rest, flags)
81
- break
82
-
83
- case 'remote':
84
- await handleRemoteCommand(rest, flags)
85
- break
86
-
87
- case 'wallet':
88
- await handleWalletCommand(rest, flags)
89
- break
90
-
91
- case 'ordinals':
92
- await handleOrdinalsCommand(rest, flags)
93
- break
94
-
95
- case 'tokens':
96
- await handleTokensCommand(rest, flags)
97
- break
98
-
99
- case 'locks':
100
- await handleLocksCommand(rest, flags)
101
- break
102
-
103
- case 'identity':
104
- await handleIdentityCommand(rest, flags)
105
- break
106
-
107
- case 'social':
108
- await handleSocialCommand(rest, flags)
109
- break
110
-
111
- case 'opns':
112
- await handleOpnsCommand(rest, flags)
113
- break
114
-
115
- case 'sweep':
116
- await handleSweepCommand(rest, flags)
117
- break
118
-
119
- case 'action':
120
- await handleActionCommand(rest, flags)
121
- break
122
-
123
- case 'tx':
124
- await handleTxCommand(rest, flags)
125
- break
126
-
127
- case 'mcp-proxy':
128
- await handleMcpProxyCommand()
129
- break
130
-
131
- case 'serve':
132
- await handleServeCommand(rest, flags)
133
- break
134
-
135
- case 'help':
136
- printHelp(flags.json)
137
- break
138
-
139
- default:
140
- console.error(formatError(`Unknown command: ${command}`))
141
- printHelp(flags.json)
142
- process.exit(1)
143
- }
144
- }
145
-
146
- main().catch((err) => {
147
- console.error(formatError(`Error: ${err.message}`))
148
- if (process.env.DEBUG) console.error(err.stack)
149
- process.exit(1)
150
- })
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/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
+ }