Configurable Port Specification
Overview
NUXP should support runtime-configurable HTTP server port, allowing users to change the port without restarting Illustrator or the frontend application.
Goals
- No restart required - Port changes apply immediately
- Persistent configuration - Port setting survives app restarts
- Generic infrastructure - Downstream apps (Flora, etc.) inherit this capability
- Graceful reconnection - Frontend handles port change seamlessly
Architecture
┌─────────────────┐ ┌──────────────────┐
│ Frontend App │ │ C++ Plugin │
│ (Tauri/Web) │ │ (Illustrator) │
├─────────────────┤ HTTP/SSE ├──────────────────┤
│ TypeScript │ ←───────────────────────→ │ HttpServer │
│ - SDK client │ localhost:PORT │ - ConfigManager │
│ - SSE client │ │ - SSE │
└─────────────────┘ └──────────────────┘
C++ Implementation
1. Config File Location
// macOS: ~/Library/Application Support/NUXP/config.json
std::string GetConfigDir() {
char* home = getenv("HOME");
return std::string(home) + "/Library/Application Support/NUXP";
}
### 2. Config File Format
```json
{
"version": 1,
"server": {
"port": 8080
}
}
3. ConfigManager Class
File: src/ConfigManager.hpp
#pragma once
#include <nlohmann/json.hpp>
#include <string>
#include <mutex>
class ConfigManager {
public:
static constexpr int DEFAULT_PORT = 8080;
/**
* Get singleton instance
*/
static ConfigManager& Instance();
/**
* Load configuration from disk.
* Creates default config if file doesn't exist.
*/
void Load();
/**
* Save current configuration to disk.
*/
void Save();
/**
* Get the configured server port.
* @return Port number (default: 8080)
*/
int GetPort() const;
/**
* Set the server port.
* Does NOT automatically restart the server - caller must do that.
* @param port New port number
*/
void SetPort(int port);
/**
* Get the full config as JSON (for /config endpoint)
*/
nlohmann::json GetConfig() const;
/**
* Get the config file path
*/
static std::string GetConfigPath();
private:
ConfigManager() = default;
mutable std::mutex mutex_;
nlohmann::json config_;
};
4. HTTP Endpoints
GET /config Returns current configuration.
{
"success": true,
"config": {
"version": 1,
"server": {
"port": 8080
}
}
}
POST /config/port Changes the server port. Server restarts on new port immediately.
Request:
{
"port": 8082
}
Response:
{
"success": true,
"previousPort": 8080,
"newPort": 8082,
"message": "Server restarted on port 8082"
}
Error Response (invalid port):
{
"success": false,
"error": "Port must be between 1024 and 65535"
}
5. Port Change Handler
std::string HandleSetPort(const std::string& body) {
auto json = nlohmann::json::parse(body);
int newPort = json["port"].get<int>();
// Validate port range
if (newPort < 1024 || newPort > 65535) {
return R"({"success": false, "error": "Port must be between 1024 and 65535"})";
}
int oldPort = ConfigManager::Instance().GetPort();
// Save new port to config
ConfigManager::Instance().SetPort(newPort);
ConfigManager::Instance().Save();
// Schedule server restart (must happen after response is sent)
// Use a short delay so the HTTP response can complete
std::thread([newPort]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
HttpServer::Stop();
HttpServer::Start(newPort);
}).detach();
nlohmann::json response;
response["success"] = true;
response["previousPort"] = oldPort;
response["newPort"] = newPort;
response["message"] = "Server restarting on port " + std::to_string(newPort);
return response.dump();
}
6. Startup Flow
// In plugin startup:
void StartPlugin() {
// Load config (creates default if missing)
ConfigManager::Instance().Load();
// Start server on configured port
int port = ConfigManager::Instance().GetPort();
HttpServer::Start(port);
}
TypeScript Implementation
1. SDK Config Extension
File: sdk/config.ts
export interface ServerConfig {
port: number;
}
// In-memory current config
let currentPort: number = DEFAULT_PORT;
/**
* Get the current server URL
*/
export function getApiUrl(path: string): string {
return `http://localhost:${currentPort}${path}`;
}
/**
* Update the port used for API calls
*/
export function setPort(port: number): void {
currentPort = port;
// Persist to localStorage for page reloads
localStorage.setItem('nuxp_port', String(port));
}
/**
* Load port from localStorage on init
*/
export function initConfig(): void {
const saved = localStorage.getItem('nuxp_port');
if (saved) {
currentPort = parseInt(saved, 10);
}
}
2. Config API Functions
File: sdk/configApi.ts
import { getApiUrl, setPort } from './config';
import { sseClient } from './sse';
export interface PortChangeResult {
success: boolean;
previousPort: number;
newPort: number;
message: string;
error?: string;
}
/**
* Get current server configuration
*/
export async function getServerConfig(): Promise<{ port: number }> {
const response = await fetch(getApiUrl('/config'));
const data = await response.json();
return data.config.server;
}
/**
* Change the server port
* Handles reconnection automatically
*/
export async function changePort(newPort: number): Promise<PortChangeResult> {
// Validate locally first
if (newPort < 1024 || newPort > 65535) {
return {
success: false,
previousPort: 0,
newPort,
message: '',
error: 'Port must be between 1024 and 65535'
};
}
try {
// Send request to current port
const response = await fetch(getApiUrl('/config/port'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ port: newPort })
});
const result: PortChangeResult = await response.json();
if (result.success) {
// Update local config
setPort(newPort);
// Reconnect SSE to new port after brief delay
// (server needs time to restart)
setTimeout(() => {
sseClient.disconnect();
sseClient.connect();
}, 500);
}
return result;
} catch (error) {
return {
success: false,
previousPort: 0,
newPort,
message: '',
error: `Failed to change port: ${error}`
};
}
}
3. SSE Reconnection
The SSE client should use getApiUrl() so it automatically uses the updated port:
// In SSE client connect():
connect(): void {
const url = getApiUrl('/events/stream'); // Uses current port
this.eventSource = new EventSource(url);
// ...
}
UI Integration (Optional for NUXP, Required for Downstream Apps)
Downstream apps like Flora can add a settings UI:
<template>
<div class="port-settings">
<label>HTTP Server Port</label>
<input v-model.number="port" type="number" min="1024" max="65535" />
<button @click="savePort" :disabled="saving">
{{ saving ? 'Saving...' : 'Save' }}
</button>
<span v-if="message" :class="{ error: isError }">{{ message }}</span>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { changePort, getServerConfig } from '@/sdk/configApi';
const port = ref(8080);
const saving = ref(false);
const message = ref('');
const isError = ref(false);
onMounted(async () => {
const config = await getServerConfig();
port.value = config.port;
});
async function savePort() {
saving.value = true;
message.value = '';
const result = await changePort(port.value);
if (result.success) {
message.value = `Port changed to ${result.newPort}`;
isError.value = false;
} else {
message.value = result.error || 'Failed to change port';
isError.value = true;
}
saving.value = false;
}
</script>
Port Discovery (Advanced)
For cases where the frontend doesn’t know the port (fresh install), implement port scanning:
const CANDIDATE_PORTS = [8080, 8081, 8082, 8083];
/**
* Find the port where NUXP plugin is running
*/
export async function discoverPort(): Promise<number | null> {
for (const port of CANDIDATE_PORTS) {
try {
const response = await fetch(`http://localhost:${port}/health`, {
signal: AbortSignal.timeout(500)
});
if (response.ok) {
const data = await response.json();
// Verify it's NUXP, not some other service
if (data.plugin === 'NUXP') {
return port;
}
}
} catch {
// Port not responding, try next
}
}
return null;
}
Update /health endpoint to include plugin identifier:
{
"success": true,
"plugin": "NUXP",
"version": "1.0.0"
}
Testing Requirements
- Unit tests for ConfigManager (load, save, get, set)
- Integration test: Change port via HTTP, verify server restarts
- E2E test: UI changes port, SSE reconnects, events flow
- Edge cases:
- Port already in use
- Invalid port numbers
- Config file permissions error
- Config file corrupted
Security Considerations
- Localhost only - Server binds to
127.0.0.1, not0.0.0.0 - Port range - Restrict to unprivileged ports (1024-65535)
- No auth required - Localhost assumption (revisit if remote access needed)
Summary
| Layer | Change |
|---|---|
| C++ | Add ConfigManager, /config endpoints, server restart logic |
| TypeScript | Add configApi.ts, update SSE to use dynamic port |
| Config File | ~/Library/Application Support/NUXP/config.json |
| Default Port | 8080 |