@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 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 # foreground (auto-builds if needed)
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 --rebuild # force rebuild
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
@@ -3,10 +3,19 @@
3
3
  * forge-server — Start the Forge web platform.
4
4
  *
5
5
  * Usage:
6
- * forge-server Start in foreground (production mode)
7
- * forge-server --dev Start in foreground (development mode)
8
- * forge-server --background Start in background (production mode), logs to ~/.forge/forge.log
9
- * forge-server --stop Stop background server
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
- const DATA_DIR = join(homedir(), '.forge');
21
- const PID_FILE = join(DATA_DIR, 'forge.pid');
22
- const LOG_FILE = join(DATA_DIR, 'forge.log');
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 ~/.forge/.env.local ──
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
- // ── Stop ──
47
- if (isStop) {
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
- // Build if needed
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('[forge] Starting in development mode...');
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('[forge] Starting server...');
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 },
@@ -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-10 border-b border-[var(--border)] flex items-center justify-between px-4 shrink-0">
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
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # dev-test.sh — Start Forge test instance (port 4000, data ~/.forge-test)
3
+
4
+ mkdir -p ~/.forge-test
5
+ PORT=4000 TERMINAL_PORT=4001 FORGE_DATA_DIR=~/.forge-test pnpm dev -- -p 4000
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
- // Check if port 3001 is already in use
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
- // Port in use — terminal server already running
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(3001);
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 SETTINGS_FILE = join(homedir(), '.forge', 'settings.yaml');
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
@@ -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
- let polling = false;
20
- let pollTimer: ReturnType<typeof setTimeout> | null = null;
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 || g[globalKey]) return;
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
- g[globalKey] = false;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
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>"