@aion0/forge 0.2.3 → 0.2.4
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/CLAUDE.md +9 -5
- package/bin/forge-server.mjs +85 -37
- package/components/Dashboard.tsx +1 -1
- package/components/NewTaskModal.tsx +1 -1
- package/dev-test.sh +5 -0
- package/lib/init.ts +5 -5
- package/lib/settings.ts +2 -1
- package/lib/telegram-bot.ts +12 -17
- package/package.json +1 -1
- package/publish.sh +45 -0
package/CLAUDE.md
CHANGED
|
@@ -19,11 +19,15 @@ npm install -g /Users/zliu/IdeaProjects/my-workflow
|
|
|
19
19
|
npm install -g @aion0/forge
|
|
20
20
|
|
|
21
21
|
# Run via npm global install
|
|
22
|
-
forge-server
|
|
23
|
-
forge-server --dev
|
|
24
|
-
forge-server --background
|
|
25
|
-
forge-server --stop
|
|
26
|
-
forge-server --
|
|
22
|
+
forge-server # foreground (default port 3000)
|
|
23
|
+
forge-server --dev # dev mode
|
|
24
|
+
forge-server --background # background, logs to ~/.forge/forge.log
|
|
25
|
+
forge-server --stop # stop background server
|
|
26
|
+
forge-server --restart # stop + start (safe for remote)
|
|
27
|
+
forge-server --rebuild # force rebuild
|
|
28
|
+
forge-server --port 4000 # custom web port
|
|
29
|
+
forge-server --terminal-port 4001 # custom terminal port
|
|
30
|
+
forge-server --dir ~/.forge-staging # custom data directory
|
|
27
31
|
|
|
28
32
|
# CLI
|
|
29
33
|
forge # help
|
package/bin/forge-server.mjs
CHANGED
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
* forge-server — Start the Forge web platform.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* forge-server
|
|
7
|
-
* forge-server --dev
|
|
8
|
-
* forge-server --background
|
|
9
|
-
* forge-server --stop
|
|
6
|
+
* forge-server Start in foreground (production)
|
|
7
|
+
* forge-server --dev Start in foreground (development)
|
|
8
|
+
* forge-server --background Start in background
|
|
9
|
+
* forge-server --stop Stop background server
|
|
10
|
+
* forge-server --restart Stop + start (safe for remote)
|
|
11
|
+
* forge-server --rebuild Force rebuild
|
|
12
|
+
* forge-server --port 4000 Custom web port (default: 3000)
|
|
13
|
+
* forge-server --terminal-port 4001 Custom terminal port (default: 3001)
|
|
14
|
+
* forge-server --dir ~/.forge-test Custom data directory (default: ~/.forge)
|
|
15
|
+
*
|
|
16
|
+
* Examples:
|
|
17
|
+
* forge-server --background --port 4000 --terminal-port 4001 --dir ~/.forge-staging
|
|
18
|
+
* forge-server --restart
|
|
10
19
|
*/
|
|
11
20
|
|
|
12
21
|
import { execSync, spawn } from 'node:child_process';
|
|
@@ -17,19 +26,32 @@ import { homedir } from 'node:os';
|
|
|
17
26
|
|
|
18
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
28
|
const ROOT = join(__dirname, '..');
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
|
|
30
|
+
// ── Parse arguments ──
|
|
31
|
+
|
|
32
|
+
function getArg(name) {
|
|
33
|
+
const idx = process.argv.indexOf(name);
|
|
34
|
+
if (idx === -1 || idx + 1 >= process.argv.length) return null;
|
|
35
|
+
return process.argv[idx + 1];
|
|
36
|
+
}
|
|
23
37
|
|
|
24
38
|
const isDev = process.argv.includes('--dev');
|
|
25
39
|
const isBackground = process.argv.includes('--background');
|
|
26
40
|
const isStop = process.argv.includes('--stop');
|
|
41
|
+
const isRestart = process.argv.includes('--restart');
|
|
27
42
|
const isRebuild = process.argv.includes('--rebuild');
|
|
28
43
|
|
|
44
|
+
const webPort = parseInt(getArg('--port')) || 3000;
|
|
45
|
+
const terminalPort = parseInt(getArg('--terminal-port')) || 3001;
|
|
46
|
+
const DATA_DIR = getArg('--dir')?.replace(/^~/, homedir()) || join(homedir(), '.forge');
|
|
47
|
+
|
|
48
|
+
const PID_FILE = join(DATA_DIR, 'forge.pid');
|
|
49
|
+
const LOG_FILE = join(DATA_DIR, 'forge.log');
|
|
50
|
+
|
|
29
51
|
process.chdir(ROOT);
|
|
30
52
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
31
53
|
|
|
32
|
-
// ── Load
|
|
54
|
+
// ── Load <data-dir>/.env.local ──
|
|
33
55
|
const envFile = join(DATA_DIR, '.env.local');
|
|
34
56
|
if (existsSync(envFile)) {
|
|
35
57
|
for (const line of readFileSync(envFile, 'utf-8').split('\n')) {
|
|
@@ -43,30 +65,74 @@ if (existsSync(envFile)) {
|
|
|
43
65
|
}
|
|
44
66
|
}
|
|
45
67
|
|
|
46
|
-
//
|
|
47
|
-
|
|
68
|
+
// Set env vars for Next.js and terminal server
|
|
69
|
+
process.env.PORT = String(webPort);
|
|
70
|
+
process.env.TERMINAL_PORT = String(terminalPort);
|
|
71
|
+
process.env.FORGE_DATA_DIR = DATA_DIR;
|
|
72
|
+
|
|
73
|
+
// ── Helper: stop running instance ──
|
|
74
|
+
function stopServer() {
|
|
48
75
|
try {
|
|
49
76
|
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim());
|
|
50
77
|
process.kill(pid, 'SIGTERM');
|
|
51
78
|
unlinkSync(PID_FILE);
|
|
52
79
|
console.log(`[forge] Stopped (pid ${pid})`);
|
|
80
|
+
return true;
|
|
53
81
|
} catch {
|
|
54
82
|
console.log('[forge] No running server found');
|
|
83
|
+
return false;
|
|
55
84
|
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Helper: start background server ──
|
|
88
|
+
function startBackground() {
|
|
89
|
+
if (!existsSync(join(ROOT, '.next'))) {
|
|
90
|
+
console.log('[forge] Building...');
|
|
91
|
+
execSync('npx next build', { cwd: ROOT, stdio: 'inherit', env: { ...process.env } });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const logFd = openSync(LOG_FILE, 'a');
|
|
95
|
+
const child = spawn('npx', ['next', 'start', '-p', String(webPort)], {
|
|
96
|
+
cwd: ROOT,
|
|
97
|
+
stdio: ['ignore', logFd, logFd],
|
|
98
|
+
env: { ...process.env },
|
|
99
|
+
detached: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
103
|
+
child.unref();
|
|
104
|
+
console.log(`[forge] Started in background (pid ${child.pid})`);
|
|
105
|
+
console.log(`[forge] Web: http://localhost:${webPort}`);
|
|
106
|
+
console.log(`[forge] Terminal: ws://localhost:${terminalPort}`);
|
|
107
|
+
console.log(`[forge] Data: ${DATA_DIR}`);
|
|
108
|
+
console.log(`[forge] Log: ${LOG_FILE}`);
|
|
109
|
+
console.log(`[forge] Stop: forge-server --stop${DATA_DIR !== join(homedir(), '.forge') ? ` --dir ${DATA_DIR}` : ''}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Stop ──
|
|
113
|
+
if (isStop) {
|
|
114
|
+
stopServer();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Restart ──
|
|
119
|
+
if (isRestart) {
|
|
120
|
+
stopServer();
|
|
121
|
+
// Brief delay to let port release
|
|
122
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
123
|
+
startBackground();
|
|
56
124
|
process.exit(0);
|
|
57
125
|
}
|
|
58
126
|
|
|
59
127
|
// ── Rebuild ──
|
|
60
128
|
if (isRebuild || existsSync(join(ROOT, '.next', 'BUILD_ID'))) {
|
|
61
|
-
// Always rebuild after npm install (new version)
|
|
62
|
-
const buildIdFile = join(ROOT, '.next', 'BUILD_ID');
|
|
63
129
|
const pkgVersion = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')).version;
|
|
64
130
|
const versionFile = join(ROOT, '.next', '.forge-version');
|
|
65
131
|
const lastBuiltVersion = existsSync(versionFile) ? readFileSync(versionFile, 'utf-8').trim() : '';
|
|
66
132
|
if (isRebuild || lastBuiltVersion !== pkgVersion) {
|
|
67
133
|
console.log(`[forge] Rebuilding (v${pkgVersion})...`);
|
|
68
134
|
execSync('rm -rf .next', { cwd: ROOT });
|
|
69
|
-
execSync('npx next build', { cwd: ROOT, stdio: 'inherit' });
|
|
135
|
+
execSync('npx next build', { cwd: ROOT, stdio: 'inherit', env: { ...process.env } });
|
|
70
136
|
writeFileSync(versionFile, pkgVersion);
|
|
71
137
|
if (isRebuild) {
|
|
72
138
|
console.log('[forge] Rebuild complete');
|
|
@@ -77,32 +143,14 @@ if (isRebuild || existsSync(join(ROOT, '.next', 'BUILD_ID'))) {
|
|
|
77
143
|
|
|
78
144
|
// ── Background ──
|
|
79
145
|
if (isBackground) {
|
|
80
|
-
|
|
81
|
-
if (!existsSync(join(ROOT, '.next'))) {
|
|
82
|
-
console.log('[forge] Building...');
|
|
83
|
-
execSync('npx next build', { cwd: ROOT, stdio: 'inherit' });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const logFd = openSync(LOG_FILE, 'a');
|
|
87
|
-
const child = spawn('npx', ['next', 'start'], {
|
|
88
|
-
cwd: ROOT,
|
|
89
|
-
stdio: ['ignore', logFd, logFd],
|
|
90
|
-
env: { ...process.env },
|
|
91
|
-
detached: true,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
writeFileSync(PID_FILE, String(child.pid));
|
|
95
|
-
child.unref();
|
|
96
|
-
console.log(`[forge] Started in background (pid ${child.pid})`);
|
|
97
|
-
console.log(`[forge] Log: ${LOG_FILE}`);
|
|
98
|
-
console.log(`[forge] Stop: forge-server --stop`);
|
|
146
|
+
startBackground();
|
|
99
147
|
process.exit(0);
|
|
100
148
|
}
|
|
101
149
|
|
|
102
150
|
// ── Foreground ──
|
|
103
151
|
if (isDev) {
|
|
104
|
-
console.log(
|
|
105
|
-
const child = spawn('npx', ['next', 'dev', '--turbopack'], {
|
|
152
|
+
console.log(`[forge] Starting dev mode (port ${webPort}, terminal ${terminalPort}, data ${DATA_DIR})`);
|
|
153
|
+
const child = spawn('npx', ['next', 'dev', '--turbopack', '-p', String(webPort)], {
|
|
106
154
|
cwd: ROOT,
|
|
107
155
|
stdio: 'inherit',
|
|
108
156
|
env: { ...process.env },
|
|
@@ -111,10 +159,10 @@ if (isDev) {
|
|
|
111
159
|
} else {
|
|
112
160
|
if (!existsSync(join(ROOT, '.next'))) {
|
|
113
161
|
console.log('[forge] Building...');
|
|
114
|
-
execSync('npx next build', { cwd: ROOT, stdio: 'inherit' });
|
|
162
|
+
execSync('npx next build', { cwd: ROOT, stdio: 'inherit', env: { ...process.env } });
|
|
115
163
|
}
|
|
116
|
-
console.log(
|
|
117
|
-
const child = spawn('npx', ['next', 'start'], {
|
|
164
|
+
console.log(`[forge] Starting server (port ${webPort}, terminal ${terminalPort}, data ${DATA_DIR})`);
|
|
165
|
+
const child = spawn('npx', ['next', 'start', '-p', String(webPort)], {
|
|
118
166
|
cwd: ROOT,
|
|
119
167
|
stdio: 'inherit',
|
|
120
168
|
env: { ...process.env },
|
package/components/Dashboard.tsx
CHANGED
|
@@ -91,7 +91,7 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
91
91
|
return (
|
|
92
92
|
<div className="h-screen flex flex-col">
|
|
93
93
|
{/* Top bar */}
|
|
94
|
-
<header className="h-
|
|
94
|
+
<header className="h-12 border-b-2 border-[var(--border)] flex items-center justify-between px-4 shrink-0 bg-[var(--bg-secondary)]">
|
|
95
95
|
<div className="flex items-center gap-4">
|
|
96
96
|
<span className="text-sm font-bold text-[var(--accent)]">Forge</span>
|
|
97
97
|
|
|
@@ -155,7 +155,7 @@ export default function NewTaskModal({
|
|
|
155
155
|
className="w-full px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
|
|
156
156
|
>
|
|
157
157
|
{projects.map(p => (
|
|
158
|
-
<option key={p.name} value={p.name}>
|
|
158
|
+
<option key={`${p.name}-${p.path}`} value={p.name}>
|
|
159
159
|
{p.name} {p.language ? `(${p.language})` : ''}
|
|
160
160
|
</option>
|
|
161
161
|
))}
|
package/dev-test.sh
ADDED
package/lib/init.ts
CHANGED
|
@@ -59,16 +59,16 @@ let terminalChild: ReturnType<typeof spawn> | null = null;
|
|
|
59
59
|
function startTerminalProcess() {
|
|
60
60
|
if (terminalChild) return;
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
const termPort = Number(process.env.TERMINAL_PORT) || 3001;
|
|
63
|
+
|
|
64
|
+
// Check if port is already in use
|
|
63
65
|
const net = require('node:net');
|
|
64
66
|
const tester = net.createServer();
|
|
65
67
|
tester.once('error', () => {
|
|
66
|
-
|
|
67
|
-
console.log('[terminal] Port 3001 already in use, skipping');
|
|
68
|
+
console.log(`[terminal] Port ${termPort} already in use, skipping`);
|
|
68
69
|
});
|
|
69
70
|
tester.once('listening', () => {
|
|
70
71
|
tester.close();
|
|
71
|
-
// Port free — start terminal server
|
|
72
72
|
const script = join(process.cwd(), 'lib', 'terminal-standalone.ts');
|
|
73
73
|
terminalChild = spawn('npx', ['tsx', script], {
|
|
74
74
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
@@ -78,5 +78,5 @@ function startTerminalProcess() {
|
|
|
78
78
|
terminalChild.on('exit', () => { terminalChild = null; });
|
|
79
79
|
console.log('[terminal] Started standalone server (pid:', terminalChild.pid, ')');
|
|
80
80
|
});
|
|
81
|
-
tester.listen(
|
|
81
|
+
tester.listen(termPort);
|
|
82
82
|
}
|
package/lib/settings.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const DATA_DIR = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
|
|
7
|
+
const SETTINGS_FILE = join(DATA_DIR, 'settings.yaml');
|
|
7
8
|
|
|
8
9
|
export interface Settings {
|
|
9
10
|
projectRoots: string[]; // Multiple project directories
|
package/lib/telegram-bot.ts
CHANGED
|
@@ -16,13 +16,11 @@ import { startTunnel, stopTunnel, getTunnelStatus } from './cloudflared';
|
|
|
16
16
|
import { getPassword } from './password';
|
|
17
17
|
import type { Task, TaskLogEntry } from '@/src/types';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let lastUpdateId = 0;
|
|
22
|
-
|
|
23
|
-
// Prevent duplicate polling across hot-reloads
|
|
24
|
-
const globalKey = Symbol.for('mw-telegram-polling');
|
|
19
|
+
// Prevent duplicate polling and state loss across hot-reloads
|
|
20
|
+
const globalKey = Symbol.for('mw-telegram-state');
|
|
25
21
|
const g = globalThis as any;
|
|
22
|
+
if (!g[globalKey]) g[globalKey] = { polling: false, pollTimer: null, lastUpdateId: 0 };
|
|
23
|
+
const botState: { polling: boolean; pollTimer: ReturnType<typeof setTimeout> | null; lastUpdateId: number } = g[globalKey];
|
|
26
24
|
|
|
27
25
|
// Track which Telegram message maps to which task (for reply-based interaction)
|
|
28
26
|
const taskMessageMap = new Map<number, string>(); // messageId → taskId
|
|
@@ -48,12 +46,11 @@ const logBuffers = new Map<string, { entries: string[]; timer: ReturnType<typeof
|
|
|
48
46
|
// ─── Start/Stop ──────────────────────────────────────────────
|
|
49
47
|
|
|
50
48
|
export function startTelegramBot() {
|
|
51
|
-
if (polling
|
|
49
|
+
if (botState.polling) return;
|
|
52
50
|
const settings = loadSettings();
|
|
53
51
|
if (!settings.telegramBotToken || !settings.telegramChatId) return;
|
|
54
52
|
|
|
55
|
-
polling = true;
|
|
56
|
-
g[globalKey] = true;
|
|
53
|
+
botState.polling = true;
|
|
57
54
|
console.log('[telegram] Bot started');
|
|
58
55
|
|
|
59
56
|
// Set bot command menu
|
|
@@ -76,39 +73,37 @@ export function startTelegramBot() {
|
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
export function stopTelegramBot() {
|
|
79
|
-
polling = false;
|
|
80
|
-
|
|
81
|
-
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
|
76
|
+
botState.polling = false;
|
|
77
|
+
if (botState.pollTimer) { clearTimeout(botState.pollTimer); botState.pollTimer = null; }
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
// ─── Polling ─────────────────────────────────────────────────
|
|
85
81
|
|
|
86
82
|
async function poll() {
|
|
87
|
-
if (!polling) return;
|
|
83
|
+
if (!botState.polling) return;
|
|
88
84
|
|
|
89
85
|
try {
|
|
90
86
|
const settings = loadSettings();
|
|
91
|
-
const url = `https://api.telegram.org/bot${settings.telegramBotToken}/getUpdates?offset=${lastUpdateId + 1}&timeout=30`;
|
|
87
|
+
const url = `https://api.telegram.org/bot${settings.telegramBotToken}/getUpdates?offset=${botState.lastUpdateId + 1}&timeout=30`;
|
|
92
88
|
const res = await fetch(url);
|
|
93
89
|
const data = await res.json();
|
|
94
90
|
|
|
95
91
|
if (data.ok && data.result) {
|
|
96
92
|
for (const update of data.result) {
|
|
97
|
-
lastUpdateId = update.update_id;
|
|
93
|
+
botState.lastUpdateId = update.update_id;
|
|
98
94
|
if (update.message?.text) {
|
|
99
95
|
await handleMessage(update.message);
|
|
100
96
|
}
|
|
101
97
|
}
|
|
102
98
|
}
|
|
103
99
|
} catch (err: any) {
|
|
104
|
-
// Network errors (ECONNRESET, fetch failed) are normal during sleep/wake — silent retry
|
|
105
100
|
const isNetworkError = err?.cause?.code === 'ECONNRESET' || err?.message?.includes('fetch failed');
|
|
106
101
|
if (!isNetworkError) {
|
|
107
102
|
console.error('[telegram] Poll error:', err);
|
|
108
103
|
}
|
|
109
104
|
}
|
|
110
105
|
|
|
111
|
-
pollTimer = setTimeout(poll, 1000);
|
|
106
|
+
botState.pollTimer = setTimeout(poll, 1000);
|
|
112
107
|
}
|
|
113
108
|
|
|
114
109
|
// ─── Message Handler ─────────────────────────────────────────
|
package/package.json
CHANGED
package/publish.sh
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# publish.sh — Bump version, commit, and publish to npm
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./publish.sh # patch bump (0.2.3 → 0.2.4)
|
|
6
|
+
# ./publish.sh minor # minor bump (0.2.3 → 0.3.0)
|
|
7
|
+
# ./publish.sh major # major bump (0.2.3 → 1.0.0)
|
|
8
|
+
# ./publish.sh 0.5.0 # explicit version
|
|
9
|
+
|
|
10
|
+
set -e
|
|
11
|
+
|
|
12
|
+
VERSION_ARG=${1:-patch}
|
|
13
|
+
CURRENT=$(node -p "require('./package.json').version")
|
|
14
|
+
|
|
15
|
+
# Calculate new version
|
|
16
|
+
if [[ "$VERSION_ARG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
17
|
+
NEW_VERSION=$VERSION_ARG
|
|
18
|
+
elif [ "$VERSION_ARG" = "patch" ]; then
|
|
19
|
+
IFS='.' read -r major minor patch <<< "$CURRENT"
|
|
20
|
+
NEW_VERSION="$major.$minor.$((patch + 1))"
|
|
21
|
+
elif [ "$VERSION_ARG" = "minor" ]; then
|
|
22
|
+
IFS='.' read -r major minor patch <<< "$CURRENT"
|
|
23
|
+
NEW_VERSION="$major.$((minor + 1)).0"
|
|
24
|
+
elif [ "$VERSION_ARG" = "major" ]; then
|
|
25
|
+
IFS='.' read -r major minor patch <<< "$CURRENT"
|
|
26
|
+
NEW_VERSION="$((major + 1)).0.0"
|
|
27
|
+
else
|
|
28
|
+
echo "Usage: ./publish.sh [patch|minor|major|x.y.z]"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo "Version: $CURRENT → $NEW_VERSION"
|
|
33
|
+
echo ""
|
|
34
|
+
|
|
35
|
+
# Update package.json
|
|
36
|
+
sed -i '' "s/\"version\": \"$CURRENT\"/\"version\": \"$NEW_VERSION\"/" package.json
|
|
37
|
+
|
|
38
|
+
# Commit
|
|
39
|
+
git add -A
|
|
40
|
+
git commit -m "v$NEW_VERSION"
|
|
41
|
+
git tag "v$NEW_VERSION"
|
|
42
|
+
|
|
43
|
+
echo ""
|
|
44
|
+
echo "Ready to publish @aion0/forge@$NEW_VERSION"
|
|
45
|
+
echo "Run: npm login && npm publish --access public --otp=<code>"
|