@aion0/forge 0.5.42 → 0.5.44

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.
Files changed (70) hide show
  1. package/RELEASE_NOTES.md +16 -4
  2. package/components/Dashboard.tsx +22 -1
  3. package/components/WorkspaceView.tsx +15 -2
  4. package/intellij-plugin/README.md +53 -0
  5. package/intellij-plugin/build.gradle.kts +60 -0
  6. package/intellij-plugin/gradle/gradle-daemon-jvm.properties +12 -0
  7. package/intellij-plugin/gradle.properties +9 -0
  8. package/intellij-plugin/publish.sh +78 -0
  9. package/intellij-plugin/settings.gradle.kts +7 -0
  10. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LoginAction.kt +49 -0
  11. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/LogoutAction.kt +18 -0
  12. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/OpenWebUIAction.kt +13 -0
  13. package/intellij-plugin/src/main/kotlin/com/aion0/forge/action/SwitchConnectionAction.kt +26 -0
  14. package/intellij-plugin/src/main/kotlin/com/aion0/forge/api/ForgeClient.kt +115 -0
  15. package/intellij-plugin/src/main/kotlin/com/aion0/forge/auth/Auth.kt +31 -0
  16. package/intellij-plugin/src/main/kotlin/com/aion0/forge/connection/ConnectionManager.kt +95 -0
  17. package/intellij-plugin/src/main/kotlin/com/aion0/forge/settings/ForgeConfigurable.kt +81 -0
  18. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/DocsView.kt +99 -0
  19. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeStatusBarWidgetFactory.kt +94 -0
  20. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeToolWindowFactory.kt +27 -0
  21. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/ForgeTreeView.kt +176 -0
  22. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/Helpers.kt +48 -0
  23. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/PipelinesView.kt +226 -0
  24. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TerminalsView.kt +309 -0
  25. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/TreeNodeData.kt +33 -0
  26. package/intellij-plugin/src/main/kotlin/com/aion0/forge/ui/toolwindow/WorkspacesView.kt +166 -0
  27. package/intellij-plugin/src/main/resources/META-INF/plugin.xml +88 -0
  28. package/intellij-plugin/src/main/resources/icons/forge.svg +3 -0
  29. package/lib/agents/index.ts +1 -1
  30. package/lib/help-docs/00-overview.md +1 -0
  31. package/lib/help-docs/11-workspace.md +2 -1
  32. package/lib/help-docs/13-ide-plugins.md +90 -0
  33. package/lib/help-docs/CLAUDE.md +3 -0
  34. package/lib/workspace/orchestrator.ts +80 -7
  35. package/lib/workspace/persistence.ts +16 -0
  36. package/lib/workspace/types.ts +13 -0
  37. package/lib/workspace/watch-manager.ts +65 -24
  38. package/lib/workspace-standalone.ts +5 -9
  39. package/next-env.d.ts +1 -1
  40. package/package.json +1 -1
  41. package/vscode-extension/.vscodeignore +11 -0
  42. package/vscode-extension/README.md +48 -0
  43. package/vscode-extension/media/icon.png +0 -0
  44. package/vscode-extension/media/icon.svg +3 -0
  45. package/vscode-extension/package-lock.json +4046 -0
  46. package/vscode-extension/package.json +514 -0
  47. package/vscode-extension/publish.sh +49 -0
  48. package/vscode-extension/src/api/client.ts +217 -0
  49. package/vscode-extension/src/auth/auth.ts +32 -0
  50. package/vscode-extension/src/commands/auth.ts +44 -0
  51. package/vscode-extension/src/commands/connection.ts +113 -0
  52. package/vscode-extension/src/commands/docs.ts +40 -0
  53. package/vscode-extension/src/commands/pipeline.ts +103 -0
  54. package/vscode-extension/src/commands/server.ts +50 -0
  55. package/vscode-extension/src/commands/smith.ts +112 -0
  56. package/vscode-extension/src/commands/task.ts +43 -0
  57. package/vscode-extension/src/commands/terminal.ts +279 -0
  58. package/vscode-extension/src/commands/workspace.ts +138 -0
  59. package/vscode-extension/src/connection/manager.ts +80 -0
  60. package/vscode-extension/src/docs/fs-provider.ts +94 -0
  61. package/vscode-extension/src/docs/result-provider.ts +33 -0
  62. package/vscode-extension/src/docs/transport.ts +22 -0
  63. package/vscode-extension/src/extension.ts +314 -0
  64. package/vscode-extension/src/statusbar.ts +70 -0
  65. package/vscode-extension/src/terminal/pseudoterm.ts +123 -0
  66. package/vscode-extension/src/views/docs.ts +145 -0
  67. package/vscode-extension/src/views/pipelines.ts +222 -0
  68. package/vscode-extension/src/views/terminals.ts +91 -0
  69. package/vscode-extension/src/views/workspaces.ts +243 -0
  70. package/vscode-extension/tsconfig.json +16 -0
package/RELEASE_NOTES.md CHANGED
@@ -1,8 +1,20 @@
1
- # Forge v0.5.42
1
+ # Forge v0.5.44
2
2
 
3
- Released: 2026-04-23
3
+ Released: 2026-04-26
4
4
 
5
- ## Changes since v0.5.41
5
+ ## Changes since v0.5.43
6
6
 
7
+ ### Features
8
+ - feat: IntelliJ plugin — Forge tool window + agent terminal launcher
9
+ - feat: vscode extension scaffold — workspace/terminal/task views + native session picker
7
10
 
8
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.41...v0.5.42
11
+ ### Documentation
12
+ - docs: add 13-ide-plugins.md covering VSCode + IntelliJ plugins
13
+ - feat(vscode-ext): connections, pipelines, docs, deep-link to web UI
14
+
15
+ ### Other
16
+ - feat(vscode-ext): connections, pipelines, docs, deep-link to web UI
17
+ - feat(vscode-ext): workspace bootstrap, daemon control, smith click + actions
18
+
19
+
20
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.43...v0.5.44
@@ -98,6 +98,20 @@ function FloatingBrowser({ onClose }: { onClose: () => void }) {
98
98
 
99
99
  export default function Dashboard({ user }: { user: any }) {
100
100
  const [viewMode, setViewMode] = useState<'tasks' | 'sessions' | 'terminal' | 'docs' | 'projects' | 'pipelines' | 'workspace' | 'skills' | 'logs' | 'usage'>('terminal');
101
+
102
+ // Honour `?view=<mode>` from the URL so external links (eg the VSCode
103
+ // extension) can deep-link straight into a section. Only views that have a
104
+ // top-level render branch are accepted. `workspace` and `sessions` live
105
+ // inside ProjectDetail, so they alias to `projects` (where you pick the
106
+ // project that contains the workspace/session).
107
+ useEffect(() => {
108
+ const raw = new URLSearchParams(window.location.search).get('view');
109
+ if (!raw) return;
110
+ const aliases: Record<string, string> = { workspace: 'projects', sessions: 'projects' };
111
+ const v = aliases[raw] || raw;
112
+ const valid = ['tasks', 'terminal', 'docs', 'projects', 'pipelines', 'skills', 'logs', 'usage'];
113
+ if (valid.includes(v)) setViewMode(v as any);
114
+ }, []);
101
115
  // workspaceProject state kept for forge:open-terminal event compatibility
102
116
  const [workspaceProject, setWorkspaceProject] = useState<{ name: string; path: string } | null>(null);
103
117
  const [browserMode, setBrowserMode] = useState<'none' | 'float' | 'right' | 'left'>('none');
@@ -148,6 +162,11 @@ export default function Dashboard({ user }: { user: any }) {
148
162
  }, []);
149
163
  useEffect(() => { refreshDisplayName(); }, [refreshDisplayName]);
150
164
 
165
+ // Reflect the current user's name in the browser tab title
166
+ useEffect(() => {
167
+ document.title = displayName && displayName !== 'Forge' ? `Forge — ${displayName}` : 'Forge';
168
+ }, [displayName]);
169
+
151
170
  // Listen for open-terminal events from ProjectManager
152
171
  useEffect(() => {
153
172
  const handler = (e: Event) => {
@@ -278,7 +297,9 @@ export default function Dashboard({ user }: { user: any }) {
278
297
  <header className="h-12 border-b-2 border-[var(--border)] flex items-center justify-between px-4 shrink-0 bg-[var(--bg-secondary)]">
279
298
  <div className="flex items-center gap-4">
280
299
  <img src="/icon.png" alt="Forge" width={28} height={28} className="rounded" />
281
- <span className="text-sm font-bold text-[var(--accent)]">Forge</span>
300
+ <span className="text-sm font-bold text-[var(--accent)]">
301
+ Forge{displayName && displayName !== 'Forge' ? ` · ${displayName}` : ''}
302
+ </span>
282
303
  {versionInfo && (
283
304
  <span className="flex items-center gap-1.5">
284
305
  <span className="text-[10px] text-[var(--text-secondary)]">v{versionInfo.current}</span>
@@ -34,6 +34,7 @@ interface AgentConfig {
34
34
  interface AgentState {
35
35
  smithStatus: 'down' | 'starting' | 'active';
36
36
  taskStatus: 'idle' | 'running' | 'done' | 'failed';
37
+ paused?: boolean;
37
38
  currentStep?: number;
38
39
  tmuxSession?: string;
39
40
  artifacts: { type: string; path?: string; summary?: string }[];
@@ -2794,6 +2795,7 @@ interface AgentNodeData {
2794
2795
  workspaceId: string | null;
2795
2796
  onRun: () => void;
2796
2797
  onPause: () => void;
2798
+ onResume: () => void;
2797
2799
  onStop: () => void;
2798
2800
  onRetry: () => void;
2799
2801
  onEdit: () => void;
@@ -3307,7 +3309,7 @@ function WorkerMascot({ taskStatus, smithStatus, seed, accentColor, theme }: { t
3307
3309
  }
3308
3310
 
3309
3311
  function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
3310
- const { config, state, colorIdx, previewLines, projectPath, workspaceId, onRun, onPause, onStop, onRetry, onEdit, onRemove, onMessage, onApprove, onShowLog, onShowMemory, onShowInbox, onOpenTerminal, onSwitchSession, onSaveAsTemplate, mascotTheme, bellOn = false, onToggleBell, inboxPending = 0, inboxFailed = 0 } = data;
3312
+ const { config, state, colorIdx, previewLines, projectPath, workspaceId, onRun, onPause, onResume, onStop, onRetry, onEdit, onRemove, onMessage, onApprove, onShowLog, onShowMemory, onShowInbox, onOpenTerminal, onSwitchSession, onSaveAsTemplate, mascotTheme, bellOn = false, onToggleBell, inboxPending = 0, inboxFailed = 0 } = data;
3311
3313
  const c = COLORS[colorIdx % COLORS.length];
3312
3314
  const smithStatus = state?.smithStatus || 'down';
3313
3315
  const taskStatus = state?.taskStatus || 'idle';
@@ -3426,10 +3428,20 @@ function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
3426
3428
  </>
3427
3429
  )}
3428
3430
  {/* Message button — send instructions to agent */}
3429
- {smithStatus === 'active' && taskStatus !== 'running' && (
3431
+ {smithStatus === 'active' && taskStatus !== 'running' && !state?.paused && (
3430
3432
  <button onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); onMessage(); }}
3431
3433
  className="text-[9px] px-1.5 py-0.5 rounded bg-blue-600/20 text-blue-400 hover:bg-blue-600/30">💬 Message</button>
3432
3434
  )}
3435
+ {/* Pause / Resume — icon-only so it doesn't widen the card */}
3436
+ {smithStatus !== 'down' && config.type !== 'input' && (
3437
+ <button onPointerDown={e => e.stopPropagation()}
3438
+ onClick={e => { e.stopPropagation(); state?.paused ? onResume() : onPause(); }}
3439
+ className={`text-[9px] px-1 ${state?.paused ? 'text-orange-400 hover:text-orange-300' : 'text-gray-600 hover:text-orange-400'}`}
3440
+ title={state?.paused
3441
+ ? 'Paused — click to resume bus pickups and watch alerts'
3442
+ : 'Pause — drop new bus messages and watch alerts as failed (in-flight task continues)'}
3443
+ >{state?.paused ? '▶' : '⏸'}</button>
3444
+ )}
3433
3445
  <div className="flex-1" />
3434
3446
  <span className="flex items-center">
3435
3447
  <button onPointerDown={e => e.stopPropagation()}
@@ -3733,6 +3745,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
3733
3745
  wsApi(workspaceId!, 'run', { agentId: agent.id });
3734
3746
  },
3735
3747
  onPause: () => wsApi(workspaceId!, 'pause', { agentId: agent.id }),
3748
+ onResume: () => wsApi(workspaceId!, 'resume', { agentId: agent.id }),
3736
3749
  onStop: () => wsApi(workspaceId!, 'stop', { agentId: agent.id }),
3737
3750
  mascotTheme,
3738
3751
  bellOn: bellAgents.has(agent.id),
@@ -0,0 +1,53 @@
1
+ # Forge IntelliJ Plugin
2
+
3
+ Native IntelliJ IDEA / JetBrains IDE integration for [Forge](https://github.com/aiwatching/forge).
4
+ Mirrors (in progress) the feature set of the VSCode extension.
5
+
6
+ ## Status — v0.1.0 (scaffold)
7
+
8
+ - [x] Project structure (Gradle Kotlin DSL + IntelliJ Platform Plugin v1.17 + Kotlin 1.9)
9
+ - [x] `plugin.xml` — registers tool window, settings page, status bar widget, actions
10
+ - [x] Multi-connection support — persisted to `forge.xml` application state
11
+ - [x] Auth via PasswordSafe (per-connection token)
12
+ - [x] HTTP client (`java.net.http.HttpClient` + Gson)
13
+ - [x] Status bar widget — shows connection name + connectivity, click switches
14
+ - [x] Settings UI — edit connections, pick active
15
+ - [x] Login / Logout / Switch Connection / Open Web UI actions
16
+
17
+ ## Coming next
18
+
19
+ - [ ] Tool window tabs:
20
+ - [ ] Workspaces — projects, smiths, daemon control, Inbox/Log expansion
21
+ - [ ] Terminals — list active forge tmux sessions, attach
22
+ - [ ] Pipelines — project-bound bindings, recent runs, node detail markdown
23
+ - [ ] Docs — file tree (local file:// or remote forge-docs:// VFS)
24
+ - [ ] Smith terminal attach (custom JediTerm session over forge WebSocket)
25
+ - [ ] Pipeline node result viewer
26
+ - [ ] Workspace bootstrap from current project root
27
+ - [ ] Send selection to forge terminal
28
+
29
+ ## Build
30
+
31
+ ```bash
32
+ cd intellij-plugin
33
+ ./gradlew buildPlugin # produces build/distributions/forge-intellij-0.1.0.zip
34
+ ./gradlew runIde # launches a sandbox IntelliJ with the plugin loaded
35
+ ```
36
+
37
+ ## Install locally
38
+
39
+ After `buildPlugin`:
40
+ 1. JetBrains IDE → Settings → Plugins → ⚙ → "Install Plugin from Disk…"
41
+ 2. Pick `build/distributions/forge-intellij-0.1.0.zip`
42
+ 3. Restart IDE
43
+ 4. View → Tool Windows → Forge
44
+
45
+ Then **Tools → Forge: Login** to authenticate against the active connection.
46
+
47
+ ## Settings
48
+
49
+ Settings → Tools → Forge:
50
+ - Connections table (add / edit / remove)
51
+ - Active connection name
52
+
53
+ Tokens are stored separately in IntelliJ's PasswordSafe — clear via **Tools → Forge: Logout**.
@@ -0,0 +1,60 @@
1
+ plugins {
2
+ id("org.jetbrains.kotlin.jvm") version "2.0.21"
3
+ id("org.jetbrains.intellij.platform") version "2.15.0"
4
+ }
5
+
6
+ group = "com.aion0.forge"
7
+ version = "0.1.19"
8
+
9
+ repositories {
10
+ mavenCentral()
11
+ intellijPlatform {
12
+ defaultRepositories()
13
+ }
14
+ }
15
+
16
+ dependencies {
17
+ intellijPlatform {
18
+ intellijIdeaCommunity("2024.1")
19
+ // Bundled terminal plugin — needed for TerminalView (smith terminal attach).
20
+ bundledPlugin("org.jetbrains.plugins.terminal")
21
+ }
22
+ }
23
+
24
+ kotlin {
25
+ jvmToolchain(17)
26
+ }
27
+
28
+ // Force every Kotlin compile task (and its worker JVM) to run on JDK 17
29
+ // instead of the host JDK (25 on this machine, which crashes Kotlin's
30
+ // internal version parser).
31
+ val jdk17Launcher = javaToolchains.launcherFor {
32
+ languageVersion = JavaLanguageVersion.of(17)
33
+ }
34
+ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
35
+ kotlinJavaToolchain.toolchain.use(jdk17Launcher)
36
+ }
37
+
38
+ intellijPlatform {
39
+ buildSearchableOptions = false
40
+
41
+ pluginConfiguration {
42
+ version = "0.1.19"
43
+ ideaVersion {
44
+ sinceBuild = "241"
45
+ // Don't pin untilBuild — keeps the plugin compatible with newer
46
+ // IDEs until the platform actually breaks something.
47
+ untilBuild = provider { null }
48
+ }
49
+ }
50
+
51
+ // `gradle publishPlugin` uploads to the JetBrains Marketplace.
52
+ // Get a permanent token at https://plugins.jetbrains.com/author/me/tokens
53
+ // and export it as JETBRAINS_MARKETPLACE_TOKEN before running.
54
+ publishing {
55
+ token = providers.environmentVariable("JETBRAINS_MARKETPLACE_TOKEN")
56
+ // Use "default" for stable releases. Switch to "beta" / "eap" for
57
+ // pre-releases (users opt in via the IDE plugin settings).
58
+ channels = listOf("default")
59
+ }
60
+ }
@@ -0,0 +1,12 @@
1
+ #This file is generated by updateDaemonJvm
2
+ toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/40b4344c056b4284246d176d9701577f/redirect
3
+ toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/466f185020ff3c07ab32696653613a8d/redirect
4
+ toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/40b4344c056b4284246d176d9701577f/redirect
5
+ toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/466f185020ff3c07ab32696653613a8d/redirect
6
+ toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/1050b2216f8beaaecc1289b17d30b586/redirect
7
+ toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/2208feeb3d4e12f412e9a450db1a842a/redirect
8
+ toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/40b4344c056b4284246d176d9701577f/redirect
9
+ toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/466f185020ff3c07ab32696653613a8d/redirect
10
+ toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/3676ee7aa5095d7f22645eb0f22ca159/redirect
11
+ toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/69a793dd932268c7d1ae9d8b855de8ed/redirect
12
+ toolchainVersion=17
@@ -0,0 +1,9 @@
1
+ kotlin.stdlib.default.dependency=false
2
+ org.gradle.jvmargs=-Xmx2g -XX:+UseG1GC
3
+ org.gradle.caching=true
4
+ org.gradle.parallel=true
5
+ # Force Gradle itself to use the toolchain-provisioned JDK 17 instead of the
6
+ # host JDK (which is 25 on this machine and breaks the Kotlin compiler).
7
+ org.gradle.java.installations.auto-download=true
8
+ # Don't let the Kotlin daemon silently fall back to the host JVM (25).
9
+ kotlin.daemon.useFallbackStrategy=false
@@ -0,0 +1,78 @@
1
+ #!/bin/bash
2
+ # Publish the Forge IntelliJ plugin to the JetBrains Marketplace.
3
+ #
4
+ # Usage:
5
+ # ./publish.sh # publish current version in build.gradle.kts
6
+ # ./publish.sh patch # bump patch (0.1.19 → 0.1.20) + publish
7
+ # ./publish.sh minor # bump minor (0.1.x → 0.2.0) + publish
8
+ # ./publish.sh major # bump major (0.x.x → 1.0.0) + publish
9
+ # ./publish.sh 0.5.0 # publish exact version
10
+ #
11
+ # Auth: get a permanent token at https://plugins.jetbrains.com/author/me/tokens
12
+ # and export it as JETBRAINS_MARKETPLACE_TOKEN. The first publish must go
13
+ # through the JetBrains Marketplace upload form for moderation; subsequent
14
+ # updates can use this script.
15
+ #
16
+ # Requires JDK 17 — Gradle's Kotlin compiler crashes on the host JDK 25.
17
+
18
+ set -euo pipefail
19
+ cd "$(dirname "$0")"
20
+
21
+ if [[ -z "${JETBRAINS_MARKETPLACE_TOKEN:-}" ]]; then
22
+ echo "ERROR: set JETBRAINS_MARKETPLACE_TOKEN env var first." >&2
23
+ echo " Get one at https://plugins.jetbrains.com/author/me/tokens" >&2
24
+ exit 1
25
+ fi
26
+
27
+ ARG="${1:-}"
28
+
29
+ bump_version() {
30
+ local kind="$1"
31
+ local current
32
+ current=$(grep -E '^version = "' build.gradle.kts | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')
33
+ local new
34
+ case "$kind" in
35
+ patch|minor|major)
36
+ local major minor patch
37
+ IFS='.' read -r major minor patch <<<"$current"
38
+ case "$kind" in
39
+ patch) patch=$((patch + 1)) ;;
40
+ minor) minor=$((minor + 1)); patch=0 ;;
41
+ major) major=$((major + 1)); minor=0; patch=0 ;;
42
+ esac
43
+ new="${major}.${minor}.${patch}"
44
+ ;;
45
+ *)
46
+ new="$kind"
47
+ ;;
48
+ esac
49
+ echo "→ Bumping $current → $new"
50
+ # Replace both `version = "..."` (top-level) and `version = "..."` (in pluginConfiguration).
51
+ sed -i.bak -E "s/version = \"$current\"/version = \"$new\"/g" build.gradle.kts
52
+ rm -f build.gradle.kts.bak
53
+ }
54
+
55
+ case "$ARG" in
56
+ "")
57
+ ;;
58
+ patch|minor|major)
59
+ bump_version "$ARG"
60
+ ;;
61
+ *)
62
+ if [[ "$ARG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
63
+ bump_version "$ARG"
64
+ else
65
+ echo "Unknown argument: $ARG" >&2
66
+ echo "Usage: $0 [patch|minor|major|x.y.z]" >&2
67
+ exit 1
68
+ fi
69
+ ;;
70
+ esac
71
+
72
+ VERSION=$(grep -E '^version = "' build.gradle.kts | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')
73
+ echo "→ Publishing version $VERSION to JetBrains Marketplace…"
74
+
75
+ JAVA_HOME="$(/usr/libexec/java_home -v 17)" gradle publishPlugin
76
+
77
+ echo "✓ Published Forge Vibe Coding v$VERSION"
78
+ echo "→ https://plugins.jetbrains.com/author/me/plugins"
@@ -0,0 +1,7 @@
1
+ // Foojay resolver auto-downloads JDK 17 if the host doesn't have one,
2
+ // so users don't need to install Java separately.
3
+ plugins {
4
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
5
+ }
6
+
7
+ rootProject.name = "forge-intellij"
@@ -0,0 +1,49 @@
1
+ package com.aion0.forge.action
2
+
3
+ import com.aion0.forge.api.ForgeClient
4
+ import com.aion0.forge.connection.ConnectionManager
5
+ import com.intellij.notification.NotificationGroupManager
6
+ import com.intellij.notification.NotificationType
7
+ import com.intellij.openapi.actionSystem.AnAction
8
+ import com.intellij.openapi.actionSystem.AnActionEvent
9
+ import com.intellij.openapi.application.ApplicationManager
10
+ import com.intellij.openapi.progress.ProgressIndicator
11
+ import com.intellij.openapi.progress.Task
12
+ import com.intellij.openapi.ui.Messages
13
+
14
+ class LoginAction : AnAction() {
15
+ override fun actionPerformed(e: AnActionEvent) {
16
+ val project = e.project
17
+ val active = ConnectionManager.get().active()
18
+ val pw = Messages.showPasswordDialog(
19
+ project,
20
+ "Admin password for ${active.name} (${active.serverUrl})",
21
+ "Forge Login",
22
+ null,
23
+ ) ?: return
24
+
25
+ ProgressManager().queue(project, "Forge: signing in") {
26
+ val res = ForgeClient.get().login(pw)
27
+ ApplicationManager.getApplication().invokeLater {
28
+ val group = NotificationGroupManager.getInstance().getNotificationGroup("Forge")
29
+ if (res.ok) {
30
+ group.createNotification("Forge: logged in to ${active.name}", NotificationType.INFORMATION)
31
+ .notify(project)
32
+ } else {
33
+ group.createNotification("Forge login failed: ${res.error}", NotificationType.ERROR)
34
+ .notify(project)
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ private class ProgressManager {
42
+ fun queue(project: com.intellij.openapi.project.Project?, title: String, work: () -> Unit) {
43
+ com.intellij.openapi.progress.ProgressManager.getInstance().run(
44
+ object : Task.Backgroundable(project, title, false) {
45
+ override fun run(indicator: ProgressIndicator) { work() }
46
+ },
47
+ )
48
+ }
49
+ }
@@ -0,0 +1,18 @@
1
+ package com.aion0.forge.action
2
+
3
+ import com.aion0.forge.api.ForgeClient
4
+ import com.aion0.forge.connection.ConnectionManager
5
+ import com.intellij.notification.NotificationGroupManager
6
+ import com.intellij.notification.NotificationType
7
+ import com.intellij.openapi.actionSystem.AnAction
8
+ import com.intellij.openapi.actionSystem.AnActionEvent
9
+
10
+ class LogoutAction : AnAction() {
11
+ override fun actionPerformed(e: AnActionEvent) {
12
+ val active = ConnectionManager.get().active()
13
+ ForgeClient.get().logout()
14
+ NotificationGroupManager.getInstance().getNotificationGroup("Forge")
15
+ .createNotification("Forge: logged out of ${active.name}", NotificationType.INFORMATION)
16
+ .notify(e.project)
17
+ }
18
+ }
@@ -0,0 +1,13 @@
1
+ package com.aion0.forge.action
2
+
3
+ import com.aion0.forge.connection.ConnectionManager
4
+ import com.intellij.ide.BrowserUtil
5
+ import com.intellij.openapi.actionSystem.AnAction
6
+ import com.intellij.openapi.actionSystem.AnActionEvent
7
+
8
+ class OpenWebUIAction : AnAction() {
9
+ override fun actionPerformed(e: AnActionEvent) {
10
+ val url = ConnectionManager.get().active().serverUrl
11
+ BrowserUtil.browse(url)
12
+ }
13
+ }
@@ -0,0 +1,26 @@
1
+ package com.aion0.forge.action
2
+
3
+ import com.aion0.forge.connection.ConnectionManager
4
+ import com.intellij.openapi.actionSystem.AnAction
5
+ import com.intellij.openapi.actionSystem.AnActionEvent
6
+ import com.intellij.openapi.ui.popup.JBPopupFactory
7
+ import com.intellij.openapi.ui.popup.PopupStep
8
+ import com.intellij.openapi.ui.popup.util.BaseListPopupStep
9
+
10
+ class SwitchConnectionAction : AnAction() {
11
+ override fun actionPerformed(e: AnActionEvent) {
12
+ val mgr = ConnectionManager.get()
13
+ val items = mgr.list().map { it.name }
14
+ if (items.isEmpty()) return
15
+ val active = mgr.active().name
16
+ val popup = JBPopupFactory.getInstance()
17
+ .createListPopup(object : BaseListPopupStep<String>("Switch Forge Connection", items) {
18
+ override fun getDefaultOptionIndex(): Int = items.indexOf(active).coerceAtLeast(0)
19
+ override fun onChosen(selectedValue: String, finalChoice: Boolean): PopupStep<*>? {
20
+ mgr.setActive(selectedValue)
21
+ return PopupStep.FINAL_CHOICE
22
+ }
23
+ })
24
+ popup.showInBestPositionFor(e.dataContext)
25
+ }
26
+ }
@@ -0,0 +1,115 @@
1
+ package com.aion0.forge.api
2
+
3
+ import com.aion0.forge.auth.Auth
4
+ import com.aion0.forge.connection.ConnectionManager
5
+ import com.google.gson.Gson
6
+ import com.google.gson.JsonElement
7
+ import com.intellij.openapi.application.ApplicationManager
8
+ import com.intellij.openapi.components.Service
9
+ import java.net.URI
10
+ import java.net.http.HttpClient
11
+ import java.net.http.HttpRequest
12
+ import java.net.http.HttpResponse
13
+ import java.time.Duration
14
+
15
+ data class ApiResult(
16
+ val ok: Boolean,
17
+ val status: Int,
18
+ val data: JsonElement? = null,
19
+ val error: String? = null,
20
+ )
21
+
22
+ /** Lightweight HTTP wrapper around forge's REST API. Uses java.net.http for
23
+ * zero extra deps; Gson is bundled in the IntelliJ Platform. */
24
+ @Service
25
+ class ForgeClient {
26
+ private val http: HttpClient = HttpClient.newBuilder()
27
+ .connectTimeout(Duration.ofSeconds(5))
28
+ // Force HTTP/1.1. forge's Next.js server doesn't always handle the
29
+ // HTTP/2 upgrade probe cleanly, which manifests as
30
+ // "http/1.1 header parser received no byte".
31
+ .version(HttpClient.Version.HTTP_1_1)
32
+ .build()
33
+ private val gson = Gson()
34
+
35
+ fun activeName(): String = ConnectionManager.get().active().name
36
+ fun baseUrl(): String = ConnectionManager.get().active().serverUrl
37
+ fun terminalUrl(): String = ConnectionManager.get().active().terminalUrl
38
+
39
+ /** Verify password against the active connection's `/api/auth/verify`.
40
+ * On success, persists the returned token in PasswordSafe. */
41
+ fun login(password: String): ApiResult {
42
+ val body = gson.toJson(mapOf("password" to password))
43
+ val req = HttpRequest.newBuilder(URI.create("${baseUrl()}/api/auth/verify"))
44
+ .header("Content-Type", "application/json")
45
+ .timeout(Duration.ofSeconds(10))
46
+ .POST(HttpRequest.BodyPublishers.ofString(body))
47
+ .build()
48
+ return runCatching {
49
+ val res = http.send(req, HttpResponse.BodyHandlers.ofString())
50
+ val parsed = res.body().takeIf { it.isNotEmpty() }?.let { gson.fromJson(it, JsonElement::class.java) }
51
+ if (res.statusCode() in 200..299 && parsed?.asJsonObject?.get("token") != null) {
52
+ val token = parsed.asJsonObject.get("token").asString
53
+ Auth.get().setToken(activeName(), token)
54
+ ApiResult(true, res.statusCode(), parsed)
55
+ } else {
56
+ val err = parsed?.asJsonObject?.get("error")?.asString ?: "HTTP ${res.statusCode()}"
57
+ ApiResult(false, res.statusCode(), parsed, err)
58
+ }
59
+ }.getOrElse { ApiResult(false, 0, error = it.message ?: "network error") }
60
+ }
61
+
62
+ fun logout() {
63
+ Auth.get().clearToken(activeName())
64
+ }
65
+
66
+ /** Quick liveness probe — does NOT require auth. */
67
+ fun ping(): Boolean = runCatching {
68
+ val req = HttpRequest.newBuilder(URI.create("${baseUrl()}/api/version"))
69
+ .timeout(Duration.ofSeconds(2))
70
+ .GET().build()
71
+ http.send(req, HttpResponse.BodyHandlers.discarding()).statusCode() in 200..299
72
+ }.getOrDefault(false)
73
+
74
+ /** Generic request — returns `ApiResult` with parsed JSON body. */
75
+ fun request(path: String, method: String = "GET", body: Any? = null): ApiResult {
76
+ val url = "${baseUrl()}$path"
77
+ return runCatching {
78
+ val token = Auth.get().getToken(activeName())
79
+ val builder = HttpRequest.newBuilder(URI.create(url))
80
+ .header("Content-Type", "application/json")
81
+ .header("Accept", "application/json")
82
+ .timeout(Duration.ofSeconds(15))
83
+ if (token != null) builder.header("X-Forge-Token", token)
84
+ when (method.uppercase()) {
85
+ "GET" -> builder.GET()
86
+ "DELETE" -> builder.DELETE()
87
+ "POST" -> builder.POST(jsonBody(body))
88
+ "PUT" -> builder.PUT(jsonBody(body))
89
+ else -> builder.method(method, jsonBody(body))
90
+ }
91
+ val res = http.send(builder.build(), HttpResponse.BodyHandlers.ofString())
92
+ val text = res.body()
93
+ val data = if (text.isNullOrBlank()) null
94
+ else runCatching { gson.fromJson(text, JsonElement::class.java) }.getOrNull()
95
+ if (res.statusCode() in 200..299) {
96
+ ApiResult(true, res.statusCode(), data)
97
+ } else {
98
+ val err = data?.asJsonObject?.get("error")?.asString ?: "HTTP ${res.statusCode()}"
99
+ ApiResult(false, res.statusCode(), data, "$err ($method $url)")
100
+ }
101
+ }.getOrElse {
102
+ ApiResult(false, 0, error = "${it.javaClass.simpleName}: ${it.message ?: "network error"} ($method $url)")
103
+ }
104
+ }
105
+
106
+ private fun jsonBody(body: Any?): HttpRequest.BodyPublisher =
107
+ if (body == null) HttpRequest.BodyPublishers.noBody()
108
+ else HttpRequest.BodyPublishers.ofString(gson.toJson(body))
109
+
110
+ companion object {
111
+ @JvmStatic
112
+ fun get(): ForgeClient =
113
+ ApplicationManager.getApplication().getService(ForgeClient::class.java)
114
+ }
115
+ }
@@ -0,0 +1,31 @@
1
+ package com.aion0.forge.auth
2
+
3
+ import com.intellij.credentialStore.CredentialAttributes
4
+ import com.intellij.credentialStore.Credentials
5
+ import com.intellij.ide.passwordSafe.PasswordSafe
6
+ import com.intellij.openapi.application.ApplicationManager
7
+ import com.intellij.openapi.components.Service
8
+
9
+ /** Per-connection token storage backed by IntelliJ's PasswordSafe (system
10
+ * keychain on macOS, KWallet/libsecret on Linux, Windows Credential Manager
11
+ * on Windows). Key shape: `forge.token.<connectionName>`. */
12
+ @Service
13
+ class Auth {
14
+ fun getToken(connectionName: String): String? =
15
+ PasswordSafe.instance.getPassword(attrs(connectionName))
16
+
17
+ fun setToken(connectionName: String, token: String) {
18
+ PasswordSafe.instance.set(attrs(connectionName), Credentials("forge", token))
19
+ }
20
+
21
+ fun clearToken(connectionName: String) {
22
+ PasswordSafe.instance.set(attrs(connectionName), null)
23
+ }
24
+
25
+ private fun attrs(name: String) = CredentialAttributes("forge.token.$name", "forge")
26
+
27
+ companion object {
28
+ @JvmStatic
29
+ fun get(): Auth = ApplicationManager.getApplication().getService(Auth::class.java)
30
+ }
31
+ }