@1sat/cli 0.0.20 → 0.0.21
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 +1 -1
- package/src/cli.ts +6 -1
- package/src/commands/config.ts +32 -2
- package/src/commands/init.ts +28 -1
- package/src/commands/remote.ts +334 -0
- package/src/config.ts +33 -4
- package/src/context.ts +33 -10
- package/src/help.ts +2 -1
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
import { parseGlobalFlags } from './args'
|
|
10
10
|
import { handleActionCommand } from './commands/action'
|
|
11
|
-
import { handleMcpProxyCommand } from './commands/mcp-proxy'
|
|
12
11
|
import { handleConfigCommand } from './commands/config'
|
|
13
12
|
import { handleIdentityCommand } from './commands/identity'
|
|
14
13
|
import { handleInitCommand } from './commands/init'
|
|
15
14
|
import { handleLocksCommand } from './commands/locks'
|
|
15
|
+
import { handleMcpProxyCommand } from './commands/mcp-proxy'
|
|
16
16
|
import { handleOpnsCommand } from './commands/opns'
|
|
17
17
|
import { handleOrdinalsCommand } from './commands/ordinals'
|
|
18
|
+
import { handleRemoteCommand } from './commands/remote'
|
|
18
19
|
import { handleSocialCommand } from './commands/social'
|
|
19
20
|
import { handleSweepCommand } from './commands/sweep'
|
|
20
21
|
import { handleTokensCommand } from './commands/tokens'
|
|
@@ -49,6 +50,10 @@ async function main(): Promise<void> {
|
|
|
49
50
|
await handleConfigCommand(rest, flags)
|
|
50
51
|
break
|
|
51
52
|
|
|
53
|
+
case 'remote':
|
|
54
|
+
await handleRemoteCommand(rest, flags)
|
|
55
|
+
break
|
|
56
|
+
|
|
52
57
|
case 'wallet':
|
|
53
58
|
await handleWalletCommand(rest, flags)
|
|
54
59
|
break
|
package/src/commands/config.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Subcommands:
|
|
5
5
|
* show - Display current configuration
|
|
6
6
|
* set - Set a configuration value
|
|
7
|
+
* unset - Remove a configuration key
|
|
7
8
|
* path - Print config directory path
|
|
8
9
|
*/
|
|
9
10
|
|
|
@@ -28,8 +29,8 @@ import {
|
|
|
28
29
|
const SETTABLE_KEYS: Array<keyof OneSatCliConfig> = [
|
|
29
30
|
'chain',
|
|
30
31
|
'dataDir',
|
|
31
|
-
'activeRemote',
|
|
32
32
|
'storageIdentityKey',
|
|
33
|
+
'monitorIntervalMinutes',
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
export async function handleConfigCommand(
|
|
@@ -43,12 +44,15 @@ export async function handleConfigCommand(
|
|
|
43
44
|
return configShow(opts)
|
|
44
45
|
case 'set':
|
|
45
46
|
return configSet(rest, opts)
|
|
47
|
+
case 'unset':
|
|
48
|
+
return configUnset(rest, opts)
|
|
46
49
|
case 'path':
|
|
47
50
|
return configPath(opts)
|
|
48
51
|
default:
|
|
49
52
|
printCommandHelp('config', {
|
|
50
53
|
show: 'Display current configuration',
|
|
51
54
|
set: 'Set a config value (e.g. 1sat config set chain test)',
|
|
55
|
+
unset: 'Remove a configuration key (e.g. 1sat config unset chain)',
|
|
52
56
|
path: 'Print config directory path',
|
|
53
57
|
})
|
|
54
58
|
if (subcommand && subcommand !== 'help') {
|
|
@@ -69,8 +73,8 @@ function configShow(opts: GlobalFlags): void {
|
|
|
69
73
|
printKeyValue({
|
|
70
74
|
chain: config.chain,
|
|
71
75
|
dataDir: config.dataDir,
|
|
72
|
-
activeRemote: config.activeRemote ?? '(not set)',
|
|
73
76
|
storageIdentityKey: config.storageIdentityKey ?? '(not set)',
|
|
77
|
+
monitorIntervalMinutes: config.monitorIntervalMinutes,
|
|
74
78
|
})
|
|
75
79
|
console.log()
|
|
76
80
|
console.log(
|
|
@@ -98,11 +102,37 @@ function configSet(args: string[], opts: GlobalFlags): void {
|
|
|
98
102
|
if (key === 'chain' && value !== 'main' && value !== 'test') {
|
|
99
103
|
fatal("chain must be 'main' or 'test'")
|
|
100
104
|
}
|
|
105
|
+
if (key === 'monitorIntervalMinutes') {
|
|
106
|
+
const n = Number(value)
|
|
107
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
108
|
+
fatal('monitorIntervalMinutes must be a non-negative number (0 to disable)')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
101
111
|
|
|
102
112
|
const updated = updateConfig({ [key]: value })
|
|
103
113
|
output(opts.json ? updated : formatSuccess(`Set ${key} = ${value}`), opts)
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
function configUnset(args: string[], opts: GlobalFlags): void {
|
|
117
|
+
const [key] = args
|
|
118
|
+
|
|
119
|
+
if (!key) {
|
|
120
|
+
fatal(
|
|
121
|
+
`Usage: 1sat config unset <key>\n\nKeys that can be unset: ${SETTABLE_KEYS.join(', ')}`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!SETTABLE_KEYS.includes(key as keyof OneSatCliConfig)) {
|
|
126
|
+
fatal(
|
|
127
|
+
`Unknown config key: ${key}\n\nKeys that can be unset: ${SETTABLE_KEYS.join(', ')}`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// For other keys, use undefined
|
|
132
|
+
const updated = updateConfig({ [key]: undefined })
|
|
133
|
+
output(opts.json ? updated : formatSuccess(`Unset ${key}`), opts)
|
|
134
|
+
}
|
|
135
|
+
|
|
106
136
|
function configPath(opts: GlobalFlags): void {
|
|
107
137
|
const dir = getConfigDir()
|
|
108
138
|
output(opts.json ? { path: dir } : dir, opts)
|
package/src/commands/init.ts
CHANGED
|
@@ -173,7 +173,33 @@ export async function handleInitCommand(
|
|
|
173
173
|
process.exit(0)
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
// 5.
|
|
176
|
+
// 5. Optional: remote storage configuration
|
|
177
|
+
const useRemote = await confirm({
|
|
178
|
+
message: 'Configure remote storage? (remote is active, local is backup)',
|
|
179
|
+
defaultValue: false,
|
|
180
|
+
})
|
|
181
|
+
let activeRemote: string | undefined
|
|
182
|
+
|
|
183
|
+
if (useRemote) {
|
|
184
|
+
const url = await text({
|
|
185
|
+
message: 'Primary remote storage URL:',
|
|
186
|
+
validate(value) {
|
|
187
|
+
if (!value) return 'Required'
|
|
188
|
+
try {
|
|
189
|
+
new URL(value)
|
|
190
|
+
} catch {
|
|
191
|
+
return 'Invalid URL'
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
if (isCancel(url)) {
|
|
196
|
+
cancel('Setup cancelled.')
|
|
197
|
+
process.exit(0)
|
|
198
|
+
}
|
|
199
|
+
activeRemote = url as string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 6. Save everything
|
|
177
203
|
ensureConfigDir()
|
|
178
204
|
|
|
179
205
|
await saveKey(wif, pw as string)
|
|
@@ -195,6 +221,7 @@ export async function handleInitCommand(
|
|
|
195
221
|
...loadConfig(),
|
|
196
222
|
chain: chain as 'main' | 'test',
|
|
197
223
|
storageIdentityKey: storageId as string,
|
|
224
|
+
activeRemote,
|
|
198
225
|
})
|
|
199
226
|
|
|
200
227
|
const pk = PrivateKey.fromWif(wif)
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote storage management commands.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* add - Add a remote as backup (validates with immediate sync)
|
|
6
|
+
* list - Show all remotes and their status
|
|
7
|
+
* delete - Remove a remote from the backup list
|
|
8
|
+
* set-active - Switch active storage to a remote or back to local
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { confirm, isCancel, text } from '@clack/prompts'
|
|
12
|
+
import type { GlobalFlags } from '../args'
|
|
13
|
+
import { loadConfig, saveConfig } from '../config'
|
|
14
|
+
import { loadContext } from '../context'
|
|
15
|
+
import { printCommandHelp } from '../help'
|
|
16
|
+
import { loadKey, resolvePassword } from '../keys'
|
|
17
|
+
import { fatal, formatSuccess, formatWarning, output } from '../output'
|
|
18
|
+
|
|
19
|
+
export async function handleRemoteCommand(
|
|
20
|
+
args: string[],
|
|
21
|
+
opts: GlobalFlags,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const [subcommand, ...rest] = args
|
|
24
|
+
|
|
25
|
+
switch (subcommand) {
|
|
26
|
+
case 'add':
|
|
27
|
+
return remoteAdd(rest, opts)
|
|
28
|
+
case 'list':
|
|
29
|
+
return remoteList(rest, opts)
|
|
30
|
+
case 'delete':
|
|
31
|
+
return remoteDelete(rest, opts)
|
|
32
|
+
case 'set-active':
|
|
33
|
+
return remoteSetActive(rest, opts)
|
|
34
|
+
default:
|
|
35
|
+
printCommandHelp('remote', {
|
|
36
|
+
add: 'Add a remote storage as backup (1sat remote add <url>)',
|
|
37
|
+
list: 'List all configured remotes and their status',
|
|
38
|
+
delete:
|
|
39
|
+
'Remove a remote from the backup list (1sat remote delete <url>)',
|
|
40
|
+
'set-active':
|
|
41
|
+
'Switch active storage (1sat remote set-active <url | local>)',
|
|
42
|
+
})
|
|
43
|
+
if (subcommand && subcommand !== 'help') {
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// remote add
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
|
|
54
|
+
let url = args[0]
|
|
55
|
+
|
|
56
|
+
if (!url) {
|
|
57
|
+
url = (await text({
|
|
58
|
+
message: 'Remote storage URL:',
|
|
59
|
+
validate(value) {
|
|
60
|
+
if (!value) return 'Required'
|
|
61
|
+
try {
|
|
62
|
+
new URL(value)
|
|
63
|
+
} catch {
|
|
64
|
+
return 'Invalid URL'
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
})) as string
|
|
68
|
+
if (isCancel(url)) {
|
|
69
|
+
fatal('Cancelled')
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
try {
|
|
73
|
+
new URL(url)
|
|
74
|
+
} catch {
|
|
75
|
+
fatal(`Invalid URL: ${url}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const privateKey = await loadKey(resolvePassword())
|
|
80
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
81
|
+
chain: opts.chain,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// For backup-only, we use StorageClient via wallet-node
|
|
86
|
+
// biome-ignore lint/suspicious/noExplicitAny: StorageClient constructor not typed in wallet-toolbox
|
|
87
|
+
const { StorageClient } = await import('@1sat/wallet-node')
|
|
88
|
+
const wallet = walletResult.wallet as any
|
|
89
|
+
const client = new (StorageClient as any)(wallet, url)
|
|
90
|
+
await walletResult.storage.addWalletStorageProvider(client)
|
|
91
|
+
|
|
92
|
+
// When adding a backup, the remote may report itself as "active" which
|
|
93
|
+
// creates a conflicting active state. Re-assert local as active before syncing.
|
|
94
|
+
if (!walletResult.storage.isActiveEnabled) {
|
|
95
|
+
const localKey = config.storageIdentityKey ?? '1sat-cli-default'
|
|
96
|
+
await walletResult.storage.setActive(localKey)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await walletResult.storage.updateBackups()
|
|
100
|
+
|
|
101
|
+
// Persist to config — connectivity will be validated on next monitor run
|
|
102
|
+
const config = loadConfig()
|
|
103
|
+
const existing = config.backups ?? []
|
|
104
|
+
if (!existing.includes(url)) {
|
|
105
|
+
saveConfig({ ...config, backups: [...existing, url] })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (opts.json) {
|
|
109
|
+
output({ url, status: 'added' }, opts)
|
|
110
|
+
} else {
|
|
111
|
+
console.log(formatSuccess(` Added ${url} as backup`))
|
|
112
|
+
console.log(
|
|
113
|
+
formatWarning(
|
|
114
|
+
' Note: Use "1sat remote set-active <url>" to make this remote the primary storage',
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
await destroy()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// remote list
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
128
|
+
const privateKey = await loadKey(resolvePassword())
|
|
129
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
130
|
+
chain: opts.chain,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const backups = walletResult.storage.getBackupStores?.() ?? []
|
|
135
|
+
const config = loadConfig()
|
|
136
|
+
|
|
137
|
+
// Use config for active determination — WalletStorageManager internal state
|
|
138
|
+
// can be misleading (a backup may appear as active after addWalletStorageProvider)
|
|
139
|
+
const isRemoteActive = Boolean(config.activeRemote)
|
|
140
|
+
|
|
141
|
+
if (opts.json) {
|
|
142
|
+
output(
|
|
143
|
+
{
|
|
144
|
+
activeStorage: isRemoteActive ? 'remote' : 'local',
|
|
145
|
+
backups: walletResult.storage.getAllStores?.() ?? [],
|
|
146
|
+
config: {
|
|
147
|
+
activeRemote: config.activeRemote ?? null,
|
|
148
|
+
backups: config.backups ?? [],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
opts,
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log()
|
|
157
|
+
console.log(` ${bold('Active Storage:')} ${isRemoteActive ? 'remote' : 'local'}`)
|
|
158
|
+
if (isRemoteActive) {
|
|
159
|
+
console.log(
|
|
160
|
+
` ${bold('Active Remote:')} ${config.activeRemote}`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
console.log()
|
|
164
|
+
if (backups.length === 0 && !config.backups?.length) {
|
|
165
|
+
console.log(' No remote storages configured')
|
|
166
|
+
} else {
|
|
167
|
+
console.log(` ${bold('Backups:')}`)
|
|
168
|
+
const known = new Set(config.backups ?? [])
|
|
169
|
+
for (const b of backups) {
|
|
170
|
+
const isKnown = known.has(b)
|
|
171
|
+
console.log(` ${isKnown ? '●' : '○'} ${b}`)
|
|
172
|
+
}
|
|
173
|
+
// Show configured but not yet connected
|
|
174
|
+
for (const url of config.backups ?? []) {
|
|
175
|
+
if (!backups.includes(url)) {
|
|
176
|
+
console.log(` ? ${url} (not connected)`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log()
|
|
181
|
+
} finally {
|
|
182
|
+
await destroy()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// remote delete
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
async function remoteDelete(args: string[], opts: GlobalFlags): Promise<void> {
|
|
191
|
+
let url = args[0]
|
|
192
|
+
|
|
193
|
+
if (!url) {
|
|
194
|
+
url = (await text({
|
|
195
|
+
message: 'Remote storage URL to remove:',
|
|
196
|
+
validate(value) {
|
|
197
|
+
if (!value) return 'Required'
|
|
198
|
+
},
|
|
199
|
+
})) as string
|
|
200
|
+
if (isCancel(url)) {
|
|
201
|
+
fatal('Cancelled')
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const config = loadConfig()
|
|
206
|
+
const backups = config.backups ?? []
|
|
207
|
+
|
|
208
|
+
if (!backups.includes(url)) {
|
|
209
|
+
fatal(`Remote not found in config: ${url}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Confirm
|
|
213
|
+
const confirmed = await confirm({
|
|
214
|
+
message: `Remove ${url} from backups?`,
|
|
215
|
+
defaultValue: false,
|
|
216
|
+
})
|
|
217
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
218
|
+
fatal('Cancelled')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
saveConfig({ ...config, backups: backups.filter((u) => u !== url) })
|
|
222
|
+
|
|
223
|
+
if (opts.json) {
|
|
224
|
+
output({ url, status: 'removed' }, opts)
|
|
225
|
+
} else {
|
|
226
|
+
console.log(formatSuccess(` Removed ${url} from backups`))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// remote set-active
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
async function remoteSetActive(
|
|
235
|
+
args: string[],
|
|
236
|
+
opts: GlobalFlags,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
let target = args[0]
|
|
239
|
+
|
|
240
|
+
if (!target) {
|
|
241
|
+
target = (await text({
|
|
242
|
+
message: 'Set active storage to (url or "local"):',
|
|
243
|
+
validate(value) {
|
|
244
|
+
if (!value) return 'Required'
|
|
245
|
+
},
|
|
246
|
+
})) as string
|
|
247
|
+
if (isCancel(target)) {
|
|
248
|
+
fatal('Cancelled')
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const config = loadConfig()
|
|
253
|
+
|
|
254
|
+
if (target === 'local') {
|
|
255
|
+
// Switch back to local
|
|
256
|
+
if (!config.activeRemote && !config.backups?.length) {
|
|
257
|
+
fatal('No remote storages configured')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const privateKey = await loadKey(resolvePassword())
|
|
261
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
262
|
+
chain: opts.chain,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// Find the local storage's storageIdentityKey
|
|
267
|
+
const localKey = config.storageIdentityKey ?? '1sat-cli-default'
|
|
268
|
+
|
|
269
|
+
if (opts.json) {
|
|
270
|
+
output({ target: 'local', status: 'migrating' }, opts)
|
|
271
|
+
} else {
|
|
272
|
+
console.log(' Switching active storage to local...')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await walletResult.storage.setActive(localKey)
|
|
276
|
+
|
|
277
|
+
// Clear activeRemote from config
|
|
278
|
+
saveConfig({ ...config, activeRemote: undefined })
|
|
279
|
+
|
|
280
|
+
if (opts.json) {
|
|
281
|
+
output(
|
|
282
|
+
{ target: 'local', status: 'active', storageIdentityKey: localKey },
|
|
283
|
+
opts,
|
|
284
|
+
)
|
|
285
|
+
} else {
|
|
286
|
+
console.log(formatSuccess(' Local storage is now active'))
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
await destroy()
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
// Switch to a remote
|
|
293
|
+
// Validate URL
|
|
294
|
+
try {
|
|
295
|
+
new URL(target)
|
|
296
|
+
} catch {
|
|
297
|
+
fatal(`Invalid URL: ${target}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const privateKey = await loadKey(resolvePassword())
|
|
301
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
302
|
+
chain: opts.chain,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
if (opts.json) {
|
|
307
|
+
output({ target, status: 'migrating' }, opts)
|
|
308
|
+
} else {
|
|
309
|
+
console.log(` Switching active storage to ${target}...`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await walletResult.migrateRemote(target)
|
|
313
|
+
|
|
314
|
+
// Persist to config
|
|
315
|
+
saveConfig({ ...config, activeRemote: target })
|
|
316
|
+
|
|
317
|
+
if (opts.json) {
|
|
318
|
+
output({ target, status: 'active' }, opts)
|
|
319
|
+
} else {
|
|
320
|
+
console.log(formatSuccess(` ${target} is now active`))
|
|
321
|
+
}
|
|
322
|
+
} finally {
|
|
323
|
+
await destroy()
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ============================================================================
|
|
329
|
+
// Helpers
|
|
330
|
+
// ============================================================================
|
|
331
|
+
|
|
332
|
+
function bold(s: string): string {
|
|
333
|
+
return `\x1b[1m${s}\x1b[0m`
|
|
334
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config management for ~/.1sat/
|
|
2
|
+
* Config management for ~/.1sat/cli/
|
|
3
3
|
*
|
|
4
4
|
* Handles persistent configuration on disk with secure file permissions.
|
|
5
5
|
*/
|
|
@@ -8,7 +8,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
8
8
|
import { homedir } from 'node:os'
|
|
9
9
|
import { join } from 'node:path'
|
|
10
10
|
|
|
11
|
-
const CONFIG_DIR = join(homedir(), '.1sat')
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.1sat', 'cli')
|
|
12
12
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
13
13
|
|
|
14
14
|
export interface OneSatCliConfig {
|
|
@@ -16,19 +16,48 @@ export interface OneSatCliConfig {
|
|
|
16
16
|
chain: 'main' | 'test'
|
|
17
17
|
/** Data directory for wallet databases */
|
|
18
18
|
dataDir: string
|
|
19
|
-
/**
|
|
19
|
+
/** Primary remote storage URL (active or backup) */
|
|
20
20
|
activeRemote?: string
|
|
21
|
+
/** Backup remote storage URLs */
|
|
22
|
+
backups?: string[]
|
|
21
23
|
/** Storage identity key for wallet persistence */
|
|
22
24
|
storageIdentityKey?: string
|
|
25
|
+
/** How often to run the monitor refresh (minutes). 0 disables auto-refresh. */
|
|
26
|
+
monitorIntervalMinutes: number
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
const DEFAULT_CONFIG: OneSatCliConfig = {
|
|
26
30
|
chain: 'main',
|
|
27
31
|
dataDir: join(CONFIG_DIR, 'data'),
|
|
32
|
+
monitorIntervalMinutes: 5,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Monitor state — stored separately so we don't rewrite config.json on every command
|
|
36
|
+
const MONITOR_STATE_FILE = join(CONFIG_DIR, 'monitor-state.json')
|
|
37
|
+
|
|
38
|
+
export interface MonitorState {
|
|
39
|
+
lastMonitorRun: number // unix ms timestamp
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function loadMonitorState(): MonitorState {
|
|
43
|
+
if (!existsSync(MONITOR_STATE_FILE)) return { lastMonitorRun: 0 }
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(MONITOR_STATE_FILE, 'utf8')
|
|
46
|
+
return JSON.parse(raw)
|
|
47
|
+
} catch {
|
|
48
|
+
return { lastMonitorRun: 0 }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function saveMonitorState(state: MonitorState): void {
|
|
53
|
+
ensureConfigDir()
|
|
54
|
+
writeFileSync(MONITOR_STATE_FILE, JSON.stringify(state, null, 2), {
|
|
55
|
+
mode: 0o600,
|
|
56
|
+
})
|
|
28
57
|
}
|
|
29
58
|
|
|
30
59
|
/**
|
|
31
|
-
* Ensure ~/.1sat/ exists with secure permissions.
|
|
60
|
+
* Ensure ~/.1sat/cli/ exists with secure permissions.
|
|
32
61
|
*/
|
|
33
62
|
export function ensureConfigDir(): void {
|
|
34
63
|
if (!existsSync(CONFIG_DIR)) {
|
package/src/context.ts
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import { type OneSatContext, createContext } from '@1sat/actions'
|
|
8
8
|
import { type NodeWalletResult, createNodeWallet } from '@1sat/wallet-node'
|
|
9
9
|
import type { PrivateKey } from '@bsv/sdk'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ensureDataDir,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadMonitorState,
|
|
14
|
+
saveMonitorState,
|
|
15
|
+
} from './config'
|
|
11
16
|
|
|
12
17
|
/** Extended context that includes cleanup */
|
|
13
18
|
export interface CliContext {
|
|
@@ -16,13 +21,35 @@ export interface CliContext {
|
|
|
16
21
|
destroy: () => Promise<void>
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Run the monitor once if the interval has elapsed.
|
|
26
|
+
* This is called lazily by commands that need current state.
|
|
27
|
+
*/
|
|
28
|
+
async function runMonitorIfStale(
|
|
29
|
+
walletResult: NodeWalletResult,
|
|
30
|
+
intervalMinutes: number,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (!walletResult.monitor) return // remote active, monitor runs remotely
|
|
33
|
+
if (intervalMinutes === 0) return // disabled by user
|
|
34
|
+
|
|
35
|
+
const state = loadMonitorState()
|
|
36
|
+
const now = Date.now()
|
|
37
|
+
const elapsed = now - state.lastMonitorRun
|
|
38
|
+
const intervalMs = intervalMinutes * 60 * 1000
|
|
39
|
+
|
|
40
|
+
if (elapsed >= intervalMs) {
|
|
41
|
+
await walletResult.monitor.runOnce()
|
|
42
|
+
saveMonitorState({ lastMonitorRun: Date.now() })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
/**
|
|
20
47
|
* Create a fully initialized OneSatContext for CLI use.
|
|
21
48
|
*
|
|
22
49
|
* Sets up:
|
|
23
50
|
* - Node wallet with SQLite storage
|
|
24
51
|
* - 1Sat services for API access
|
|
25
|
-
* - Monitor for transaction lifecycle
|
|
52
|
+
* - Monitor for transaction lifecycle (lazy, interval-based)
|
|
26
53
|
*/
|
|
27
54
|
export async function loadContext(
|
|
28
55
|
privateKey: PrivateKey,
|
|
@@ -37,17 +64,13 @@ export async function loadContext(
|
|
|
37
64
|
privateKey,
|
|
38
65
|
chain: opts.chain,
|
|
39
66
|
storageIdentityKey,
|
|
40
|
-
|
|
41
|
-
client: 'better-sqlite3',
|
|
42
|
-
connection: {
|
|
43
|
-
filename: `${dataDir}/wallet-${opts.chain}.db`,
|
|
44
|
-
},
|
|
45
|
-
useNullAsDefault: true,
|
|
46
|
-
},
|
|
67
|
+
filename: `${dataDir}/wallet-${opts.chain}.db`,
|
|
47
68
|
activeRemote: config.activeRemote,
|
|
69
|
+
backups: config.backups,
|
|
48
70
|
})
|
|
49
71
|
|
|
50
|
-
|
|
72
|
+
// Run monitor once if interval has elapsed (lazy refresh)
|
|
73
|
+
await runMonitorIfStale(walletResult, config.monitorIntervalMinutes)
|
|
51
74
|
|
|
52
75
|
const ctx = createContext(walletResult.wallet, {
|
|
53
76
|
services: walletResult.services,
|
package/src/help.ts
CHANGED
|
@@ -38,6 +38,7 @@ ${bold('Usage:')}
|
|
|
38
38
|
${bold('Setup:')}
|
|
39
39
|
${cyan('init')} Interactive wallet setup wizard
|
|
40
40
|
${cyan('config')} <subcommand> Manage configuration
|
|
41
|
+
${cyan('remote')} <subcommand> Manage remote storage
|
|
41
42
|
|
|
42
43
|
${bold('Wallet:')}
|
|
43
44
|
${cyan('wallet balance')} Show wallet balance
|
|
@@ -100,7 +101,7 @@ ${bold('Environment Variables:')}
|
|
|
100
101
|
${dim('PRIVATE_KEY_WIF')} Private key (bypasses encrypted keyfile)
|
|
101
102
|
${dim('ONESAT_PASSWORD')} Password for encrypted keyfile
|
|
102
103
|
|
|
103
|
-
${bold('Config:')} ~/.1sat/
|
|
104
|
+
${bold('Config:')} ~/.1sat/cli/
|
|
104
105
|
`)
|
|
105
106
|
}
|
|
106
107
|
|