@1sat/cli 0.0.77 → 0.0.79
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 +5 -5
- package/src/cli.ts +4 -144
- package/src/commands/serve.ts +1 -1
- package/src/context.ts +18 -10
- package/src/main.ts +158 -0
- package/src/monitor-lock.ts +19 -6
- package/src/monitor-once.ts +135 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1sat/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.79",
|
|
4
4
|
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/cli.ts",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
],
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@1sat/actions": "0.0.
|
|
30
|
-
"@1sat/client": "0.0.
|
|
29
|
+
"@1sat/actions": "0.0.185",
|
|
30
|
+
"@1sat/client": "0.0.43",
|
|
31
31
|
"@1sat/types": "0.0.34",
|
|
32
|
-
"@1sat/wallet-node": "0.0.
|
|
33
|
-
"@1sat/wallet-server": "0.0.
|
|
32
|
+
"@1sat/wallet-node": "0.0.58",
|
|
33
|
+
"@1sat/wallet-server": "0.0.27",
|
|
34
34
|
"@bsv/sdk": "^2.1.6",
|
|
35
35
|
"@bsv/wallet-toolbox": "2.1.24",
|
|
36
36
|
"chalk": "^5.0.0",
|
package/src/cli.ts
CHANGED
|
@@ -1,150 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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')
|
package/src/commands/serve.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
* -
|
|
27
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
+
})
|
package/src/monitor-lock.ts
CHANGED
|
@@ -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
|
-
* -
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|