project-state-manager 1.0.1

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 (37) hide show
  1. package/README.md +51 -0
  2. package/dist/shared/schemas/process.schema.d.ts +969 -0
  3. package/dist/shared/schemas/process.schema.js +119 -0
  4. package/dist/src/core/error-formatter.d.ts +31 -0
  5. package/dist/src/core/error-formatter.js +97 -0
  6. package/dist/src/core/signal-calculator.d.ts +12 -0
  7. package/dist/src/core/signal-calculator.js +19 -0
  8. package/dist/src/core/yaml-engine.d.ts +15 -0
  9. package/dist/src/core/yaml-engine.js +162 -0
  10. package/dist/src/gui-worker.d.ts +1 -0
  11. package/dist/src/gui-worker.js +221 -0
  12. package/dist/src/index.d.ts +2 -0
  13. package/dist/src/index.js +281 -0
  14. package/dist/src/infra/ipc-hub.d.ts +18 -0
  15. package/dist/src/infra/ipc-hub.js +71 -0
  16. package/dist/src/infra/proxy-client.d.ts +32 -0
  17. package/dist/src/infra/proxy-client.js +134 -0
  18. package/dist/src/tools/check-readiness.d.ts +17 -0
  19. package/dist/src/tools/check-readiness.js +48 -0
  20. package/dist/src/tools/define-process.d.ts +14 -0
  21. package/dist/src/tools/define-process.js +39 -0
  22. package/dist/src/tools/discover-process.d.ts +7 -0
  23. package/dist/src/tools/discover-process.js +41 -0
  24. package/dist/src/tools/get-health-status.d.ts +11 -0
  25. package/dist/src/tools/get-health-status.js +62 -0
  26. package/dist/src/tools/list-existing-processes.d.ts +7 -0
  27. package/dist/src/tools/list-existing-processes.js +25 -0
  28. package/dist/src/tools/scan-node.d.ts +14 -0
  29. package/dist/src/tools/scan-node.js +47 -0
  30. package/dist/src/tools/submit-node-data.d.ts +18 -0
  31. package/dist/src/tools/submit-node-data.js +86 -0
  32. package/gui/README.md +73 -0
  33. package/gui/dist/assets/index-BpHg-2LI.css +1 -0
  34. package/gui/dist/assets/index-Cq6Ut9u2.js +9 -0
  35. package/gui/dist/index.html +14 -0
  36. package/gui/dist/vite.svg +1 -0
  37. package/package.json +55 -0
@@ -0,0 +1,119 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schema for Process nodes (Input, Flow, Output).
4
+ */
5
+ /**
6
+ * Individual schema for Entry node findings.
7
+ * Exported for GUI reuse (Story 3.1).
8
+ */
9
+ export const EntryFindingsSchema = z.object({
10
+ type: z.literal("entry"),
11
+ sources: z.array(z.string()),
12
+ validation_rules: z.array(z.string()),
13
+ sensitive_data: z.boolean(),
14
+ });
15
+ /**
16
+ * Individual schema for Flow node findings.
17
+ * Exported for GUI reuse (Story 3.1).
18
+ */
19
+ export const FlowFindingsSchema = z.object({
20
+ type: z.literal("flow"),
21
+ transformations: z.array(z.string()),
22
+ affected_services: z.array(z.string()),
23
+ side_effects: z.array(z.string()),
24
+ });
25
+ /**
26
+ * Individual schema for Exit node findings.
27
+ * Exported for GUI reuse (Story 3.1).
28
+ */
29
+ export const ExitFindingsSchema = z.object({
30
+ type: z.literal("exit"),
31
+ destinations: z.array(z.string()),
32
+ response_formats: z.array(z.string()),
33
+ error_handling: z.array(z.string()),
34
+ });
35
+ /**
36
+ * Discriminated union of all node findings schemas.
37
+ */
38
+ export const NodeFindingsSchema = z.discriminatedUnion("type", [
39
+ EntryFindingsSchema,
40
+ FlowFindingsSchema,
41
+ ExitFindingsSchema,
42
+ ]);
43
+ export const ProcessNodeSchema = z.object({
44
+ content: z.string(),
45
+ status: z.enum(["todo", "in-progress", "done", "valid", "error"]),
46
+ lastUpdated: z.string().datetime(),
47
+ manuallyModified: z.boolean().optional(),
48
+ data: NodeFindingsSchema.optional(),
49
+ });
50
+ export const ProcessSchema = z.object({
51
+ id: z.string(),
52
+ name: z.string(),
53
+ entryPoint: z.string(),
54
+ nodes: z.object({
55
+ input: ProcessNodeSchema,
56
+ flow: ProcessNodeSchema,
57
+ output: ProcessNodeSchema,
58
+ }),
59
+ is_executable: z.boolean().default(false),
60
+ });
61
+ /**
62
+ * Schema for IPC messages between Master and Worker.
63
+ * Follows the standard: { type: string, data: any, timestamp: string }
64
+ */
65
+ export const IPCMessageSchema = z.object({
66
+ type: z.string(),
67
+ correlationId: z.string().optional(),
68
+ data: z.any().optional(),
69
+ error: z.string().optional(),
70
+ timestamp: z.string().datetime(),
71
+ });
72
+ /**
73
+ * Schema for Proxy registration responses.
74
+ */
75
+ export const ProxyResponseSchema = z.object({
76
+ port: z.number().int().positive(),
77
+ url: z.string().url().optional(),
78
+ });
79
+ /**
80
+ * Tool Argument Schemas
81
+ */
82
+ export const DefineProcessArgsSchema = z.object({
83
+ name: z.string().describe('The name of the process (feature name)'),
84
+ entry_point: z.string().describe('The main file or directory associated with this process'),
85
+ });
86
+ export const DiscoverProcessArgsSchema = z.object({
87
+ context: z.string().describe('A keyword, file path, or description to find a relevant process'),
88
+ });
89
+ export const ScanNodeArgsSchema = z.object({
90
+ process_name: z.string().describe('The name (ID) of the process to scan'),
91
+ node_type: z.enum(['entry', 'flow', 'exit']).describe('The type of node to analyze (entry, flow, or exit)'),
92
+ });
93
+ export const SubmitNodeDataArgsSchema = z.object({
94
+ process_name: z.string().describe('The name (ID) of the process'),
95
+ node_type: z.enum(['entry', 'flow', 'exit']).describe('The type of node'),
96
+ findings: z.any().describe('The findings to submit (will be validated against specific schema)'),
97
+ });
98
+ export const CheckReadinessArgsSchema = z.object({
99
+ process_name: z.string().describe('The name (ID) of the process to check readiness for'),
100
+ });
101
+ export const HealthStatusSchema = z.object({
102
+ master: z.object({
103
+ status: z.string(),
104
+ uptime: z.number(),
105
+ memory: z.object({
106
+ rss: z.number(),
107
+ heapUsed: z.number(),
108
+ }),
109
+ }),
110
+ worker: z.object({
111
+ status: z.string(),
112
+ connected: z.boolean(),
113
+ uptime: z.number().optional(),
114
+ memory: z.object({
115
+ rss: z.number(),
116
+ heapUsed: z.number(),
117
+ }).optional(),
118
+ }).optional(),
119
+ });
@@ -0,0 +1,31 @@
1
+ import { ZodError } from 'zod';
2
+ /**
3
+ * Interface pour une erreur formatée avec suggestions.
4
+ */
5
+ export interface FormattedError {
6
+ path: string;
7
+ message: string;
8
+ expected?: string;
9
+ suggestion?: string;
10
+ }
11
+ /**
12
+ * Formateur d'erreurs intelligent fournissant des suggestions contextuelles pour les agents IA.
13
+ */
14
+ export declare class ErrorFormatter {
15
+ /**
16
+ * Formate une ZodError en une chaîne structurée avec des suggestions.
17
+ *
18
+ * @param error - L'instance ZodError
19
+ * @param nodeType - Le type de nœud (entry, flow, exit) pour le contexte
20
+ * @returns Une chaîne lisible par l'humain formatée pour la consommation par l'IA
21
+ */
22
+ static format(error: ZodError, nodeType: string): string;
23
+ /**
24
+ * Construit la chaîne de sortie finale avec des sections claires.
25
+ */
26
+ private static buildOutputString;
27
+ /**
28
+ * Fournit des données d'exemple basées sur le type de nœud.
29
+ */
30
+ private static getExampleData;
31
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Formateur d'erreurs intelligent fournissant des suggestions contextuelles pour les agents IA.
3
+ */
4
+ export class ErrorFormatter {
5
+ /**
6
+ * Formate une ZodError en une chaîne structurée avec des suggestions.
7
+ *
8
+ * @param error - L'instance ZodError
9
+ * @param nodeType - Le type de nœud (entry, flow, exit) pour le contexte
10
+ * @returns Une chaîne lisible par l'humain formatée pour la consommation par l'IA
11
+ */
12
+ static format(error, nodeType) {
13
+ const formattedErrors = error.errors.map((err) => {
14
+ const path = err.path.join('.');
15
+ let suggestion = '';
16
+ let expected = '';
17
+ // Suggestions contextuelles basées sur les codes d'erreur Zod
18
+ switch (err.code) {
19
+ case 'invalid_type':
20
+ expected = `${err.expected}`;
21
+ suggestion = `Le champ "${path}" doit être de type ${err.expected}. Vous avez fourni ${err.received}.`;
22
+ break;
23
+ case 'too_small':
24
+ suggestion = `Le champ "${path}" est trop court ou contient trop peu d'éléments (minimum attendu: ${err.minimum}).`;
25
+ break;
26
+ case 'too_big':
27
+ suggestion = `Le champ "${path}" est trop long ou contient trop d'éléments (maximum autorisé: ${err.maximum}).`;
28
+ break;
29
+ case 'invalid_enum_value':
30
+ expected = `L'une des valeurs: ${err.options.join(', ')}`;
31
+ suggestion = `La valeur fournie pour "${path}" n'est pas valide. Valeurs acceptées: ${err.options.join(', ')}.`;
32
+ break;
33
+ case 'invalid_string':
34
+ suggestion = `Le format de la chaîne pour "${path}" est invalide (attendu: ${err.validation}).`;
35
+ break;
36
+ case 'invalid_date':
37
+ suggestion = `La date fournie pour "${path}" est invalide.`;
38
+ break;
39
+ default:
40
+ suggestion = `Veuillez vérifier la structure du champ "${path}". ${err.message}`;
41
+ }
42
+ return {
43
+ path,
44
+ message: err.message,
45
+ expected,
46
+ suggestion
47
+ };
48
+ });
49
+ return this.buildOutputString(formattedErrors, nodeType);
50
+ }
51
+ /**
52
+ * Construit la chaîne de sortie finale avec des sections claires.
53
+ */
54
+ static buildOutputString(errors, nodeType) {
55
+ let output = `ERREUR DE VALIDATION - Noeud: ${nodeType}\n`;
56
+ output += `-------------------------------------------\n`;
57
+ errors.forEach((err) => {
58
+ output += `CHEMIN: ${err.path}\n`;
59
+ output += `PROBLEME: ${err.message}\n`;
60
+ if (err.expected)
61
+ output += `TYPE ATTENDU: ${err.expected}\n`;
62
+ if (err.suggestion)
63
+ output += `\nSUGGESTION DE CORRECTION:\n${err.suggestion}\n`;
64
+ output += `-------------------------------------------\n`;
65
+ });
66
+ output += `\nEXEMPLE DE DONNEES VALIDES POUR LE NOEUD "${nodeType}":\n`;
67
+ output += this.getExampleData(nodeType);
68
+ return output;
69
+ }
70
+ /**
71
+ * Fournit des données d'exemple basées sur le type de nœud.
72
+ */
73
+ static getExampleData(nodeType) {
74
+ switch (nodeType) {
75
+ case 'entry':
76
+ return JSON.stringify({
77
+ sources: ["src/auth/login.ts", "src/auth/register.ts"],
78
+ validation_rules: ["Vérification du format email", "Longueur minimale password"],
79
+ sensitive_data: true
80
+ }, null, 2);
81
+ case 'flow':
82
+ return JSON.stringify({
83
+ transformations: ["Hachage du mot de passe via bcrypt", "Génération du token JWT"],
84
+ affected_services: ["DatabaseService", "AuthService"],
85
+ side_effects: ["Log de connexion", "Mise à jour last_login"]
86
+ }, null, 2);
87
+ case 'exit':
88
+ return JSON.stringify({
89
+ destinations: ["Client Frontend", "Audit Log"],
90
+ response_formats: ["JSON { token: string, user: Object }"],
91
+ error_handling: ["401 Unauthorized sur échec auth", "500 Internal Error sur DB failure"]
92
+ }, null, 2);
93
+ default:
94
+ return "{}";
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,12 @@
1
+ import { Process } from '../../shared/schemas/process.schema.js';
2
+ /**
3
+ * Calculates if a process is ready for implementation (executable).
4
+ *
5
+ * Logic: A process is executable if all three fundamental nodes
6
+ * (Input, Flow, Output) are present, marked as 'done' or 'valid',
7
+ * AND contain valid findings data.
8
+ *
9
+ * @param process - The process state to evaluate
10
+ * @returns boolean indicating if the process is executable
11
+ */
12
+ export declare function calculateExecutableSignal(process: Process): boolean;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Calculates if a process is ready for implementation (executable).
3
+ *
4
+ * Logic: A process is executable if all three fundamental nodes
5
+ * (Input, Flow, Output) are present, marked as 'done' or 'valid',
6
+ * AND contain valid findings data.
7
+ *
8
+ * @param process - The process state to evaluate
9
+ * @returns boolean indicating if the process is executable
10
+ */
11
+ export function calculateExecutableSignal(process) {
12
+ const { input, flow, output } = process.nodes;
13
+ // Rule 1: All three fundamental nodes must be 'done' or 'valid'
14
+ // Rule 2: All three fundamental nodes must have findings data attached
15
+ const isInputReady = (input.status === 'done' || input.status === 'valid') && !!input.data;
16
+ const isFlowReady = (flow.status === 'done' || flow.status === 'valid') && !!flow.data;
17
+ const isOutputReady = (output.status === 'done' || output.status === 'valid') && !!output.data;
18
+ return isInputReady && isFlowReady && isOutputReady;
19
+ }
@@ -0,0 +1,15 @@
1
+ import { Process } from '../../shared/schemas/process.schema.js';
2
+ export declare class YamlEngine {
3
+ private psmDir;
4
+ private onSaveCallback?;
5
+ constructor(psmDir?: string);
6
+ onSave(callback: (process: Process) => void): void;
7
+ init(): void;
8
+ saveProcess(process: Process): Promise<void>;
9
+ private notifyUpdate;
10
+ loadProcess(id: string): Promise<Process | null>;
11
+ private ensureDir;
12
+ listProcesses(): Promise<string[]>;
13
+ getAllProcesses(): Promise<Process[]>;
14
+ updateNode(processId: string, nodeType: string, content: string, isManual?: boolean): Promise<Process>;
15
+ }
@@ -0,0 +1,162 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import YAML from 'yaml';
4
+ import { ProcessSchema } from '../../shared/schemas/process.schema.js';
5
+ import { calculateExecutableSignal } from './signal-calculator.js';
6
+ export class YamlEngine {
7
+ psmDir;
8
+ onSaveCallback;
9
+ constructor(psmDir = '.psm') {
10
+ this.psmDir = psmDir;
11
+ }
12
+ onSave(callback) {
13
+ this.onSaveCallback = callback;
14
+ }
15
+ init() {
16
+ if (!fs.existsSync(this.psmDir)) {
17
+ fs.mkdirSync(this.psmDir, { recursive: true });
18
+ }
19
+ }
20
+ async saveProcess(process) {
21
+ const validated = ProcessSchema.parse(process);
22
+ // Story 4.1: Recalculate executable signal before saving
23
+ validated.is_executable = calculateExecutableSignal(validated);
24
+ // NFR6: Prevent Path Traversal by sanitizing ID and validating it doesn't contain path separators
25
+ const safeId = path.basename(validated.id);
26
+ if (safeId !== validated.id || validated.id.includes('..')) {
27
+ throw new Error(`Invalid process ID: ${validated.id}`);
28
+ }
29
+ const filePath = path.join(this.psmDir, `${safeId}.yaml`);
30
+ const lockPath = `${filePath}.lock`;
31
+ this.ensureDir();
32
+ // Robust file-based locking with staleness detection
33
+ const lockTimeout = 5000; // 5 seconds
34
+ const retryInterval = 100;
35
+ const maxAttempts = 50;
36
+ let attempts = 0;
37
+ let locked = false;
38
+ while (attempts < maxAttempts) {
39
+ try {
40
+ // Atomic creation of lock file
41
+ fs.writeFileSync(lockPath, global.process.pid.toString(), { flag: 'wx' });
42
+ locked = true;
43
+ break;
44
+ }
45
+ catch (err) {
46
+ if (err.code === 'EEXIST') {
47
+ // Check if lock is stale
48
+ try {
49
+ const stats = fs.statSync(lockPath);
50
+ const age = Date.now() - stats.mtimeMs;
51
+ if (age > lockTimeout) {
52
+ try {
53
+ fs.unlinkSync(lockPath);
54
+ continue; // Retry immediately after clearing stale lock
55
+ }
56
+ catch (unlinkErr) {
57
+ // Ignore error if another process already deleted it
58
+ }
59
+ }
60
+ }
61
+ catch (statErr) {
62
+ if (statErr.code === 'ENOENT') {
63
+ continue; // Lock was deleted by another process, retry acquisition
64
+ }
65
+ throw statErr;
66
+ }
67
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
68
+ attempts++;
69
+ }
70
+ else {
71
+ throw err;
72
+ }
73
+ }
74
+ }
75
+ if (!locked) {
76
+ throw new Error(`Could not acquire lock for ${filePath} after ${maxAttempts} attempts`);
77
+ }
78
+ try {
79
+ const yamlContent = YAML.stringify(validated);
80
+ fs.writeFileSync(filePath, yamlContent, 'utf8');
81
+ // Notify (to be integrated with IPC Hub later)
82
+ this.notifyUpdate(validated);
83
+ }
84
+ finally {
85
+ if (locked && fs.existsSync(lockPath)) {
86
+ try {
87
+ fs.unlinkSync(lockPath);
88
+ }
89
+ catch (err) {
90
+ // Ignore unlink errors in finally
91
+ }
92
+ }
93
+ }
94
+ }
95
+ notifyUpdate(updatedProcess) {
96
+ if (this.onSaveCallback) {
97
+ this.onSaveCallback(updatedProcess);
98
+ }
99
+ if (global.process.env.DEBUG) {
100
+ console.error(`[YamlEngine] State updated for ${updatedProcess.id}`);
101
+ }
102
+ }
103
+ async loadProcess(id) {
104
+ // NFR6: Prevent Path Traversal
105
+ const safeId = path.basename(id);
106
+ if (safeId !== id || id.includes('..')) {
107
+ throw new Error(`Invalid process ID: ${id}`);
108
+ }
109
+ const filePath = path.join(this.psmDir, `${safeId}.yaml`);
110
+ if (!fs.existsSync(filePath)) {
111
+ return null;
112
+ }
113
+ const yamlContent = fs.readFileSync(filePath, 'utf8');
114
+ const data = YAML.parse(yamlContent);
115
+ return ProcessSchema.parse(data);
116
+ }
117
+ ensureDir() {
118
+ if (!fs.existsSync(this.psmDir)) {
119
+ fs.mkdirSync(this.psmDir, { recursive: true });
120
+ }
121
+ }
122
+ async listProcesses() {
123
+ if (!fs.existsSync(this.psmDir)) {
124
+ return [];
125
+ }
126
+ const files = fs.readdirSync(this.psmDir);
127
+ return files
128
+ .filter(file => file.endsWith('.yaml'))
129
+ .map(file => file.replace('.yaml', ''));
130
+ }
131
+ async getAllProcesses() {
132
+ const ids = await this.listProcesses();
133
+ const processPromises = ids.map(id => this.loadProcess(id));
134
+ const results = await Promise.all(processPromises);
135
+ return results.filter((p) => p !== null);
136
+ }
137
+ async updateNode(processId, nodeType, content, isManual = false) {
138
+ const process = await this.loadProcess(processId);
139
+ if (!process) {
140
+ throw new Error(`Process not found: ${processId}`);
141
+ }
142
+ // Map node type
143
+ let targetKey;
144
+ if (nodeType === 'entry' || nodeType === 'input')
145
+ targetKey = 'input';
146
+ else if (nodeType === 'flow')
147
+ targetKey = 'flow';
148
+ else if (nodeType === 'exit' || nodeType === 'output')
149
+ targetKey = 'output';
150
+ else
151
+ throw new Error(`Invalid node type: ${nodeType}`);
152
+ // Update node content
153
+ process.nodes[targetKey].content = content;
154
+ process.nodes[targetKey].lastUpdated = new Date().toISOString();
155
+ if (isManual) {
156
+ process.nodes[targetKey].manuallyModified = true;
157
+ }
158
+ // Re-validate and save
159
+ await this.saveProcess(process);
160
+ return process;
161
+ }
162
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,221 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { serveStatic } from "@hono/node-server/serve-static";
3
+ import { Hono } from "hono";
4
+ import { streamSSE } from "hono/streaming";
5
+ import { ProxyClient } from "./infra/proxy-client.js";
6
+ import { IPCMessageSchema } from "../shared/schemas/process.schema.js";
7
+ import path from "path";
8
+ import fs from "fs";
9
+ import { fileURLToPath } from "url";
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ // Resolve root directory reliably (works in both dev and compiled mode)
12
+ function findProjectRoot(startDir) {
13
+ let current = startDir;
14
+ while (current !== path.dirname(current)) {
15
+ if (fs.existsSync(path.join(current, "gui")) || fs.existsSync(path.join(current, "package.json"))) {
16
+ return current;
17
+ }
18
+ current = path.dirname(current);
19
+ }
20
+ return startDir;
21
+ }
22
+ const rootDir = findProjectRoot(__dirname);
23
+ const staticRoot = path.join(rootDir, "gui/dist");
24
+ const absoluteStaticRoot = path.resolve(staticRoot);
25
+ const app = new Hono();
26
+ let proxyClient = null;
27
+ const sseControllers = new Set();
28
+ // Tracking for IPC request-response
29
+ const pendingRequests = new Map();
30
+ /**
31
+ * Setup IPC error handlers
32
+ */
33
+ function setupIpcErrorHandlers() {
34
+ process.on('error', (err) => {
35
+ if (err.code === 'ERR_IPC_CHANNEL_CLOSED') {
36
+ console.error('[DEBUG] [GUI-WORKER] IPC channel closed.');
37
+ return;
38
+ }
39
+ console.error(`[DEBUG] [GUI-WORKER] Process error: ${err.message}`);
40
+ });
41
+ process.on('disconnect', () => {
42
+ console.error('[DEBUG] [GUI-WORKER] Disconnected from parent, exiting.');
43
+ process.exit(0);
44
+ });
45
+ process.on('message', (msg) => {
46
+ // Handle responses from Master
47
+ if (msg && msg.correlationId && pendingRequests.has(msg.correlationId)) {
48
+ const { resolve } = pendingRequests.get(msg.correlationId);
49
+ pendingRequests.delete(msg.correlationId);
50
+ resolve(msg);
51
+ return;
52
+ }
53
+ if (msg && msg.type === 'GET_HEALTH') {
54
+ const memory = process.memoryUsage();
55
+ process.send({
56
+ type: 'HEALTH_RESPONSE',
57
+ correlationId: msg.correlationId,
58
+ data: {
59
+ status: 'ok',
60
+ uptime: process.uptime(),
61
+ memory: {
62
+ rss: memory.rss,
63
+ heapUsed: memory.heapUsed,
64
+ },
65
+ },
66
+ timestamp: new Date().toISOString()
67
+ });
68
+ return;
69
+ }
70
+ if (msg && (msg.type === 'STATE_UPDATED' || msg.type === 'INITIAL_STATE')) {
71
+ const typeLabel = msg.type === 'STATE_UPDATED' ? `update for ${msg.data.id}` : `initial state (${msg.data.processes.length} items)`;
72
+ for (const controller of sseControllers) {
73
+ try {
74
+ controller.writeSSE({
75
+ data: JSON.stringify(msg),
76
+ event: "message",
77
+ });
78
+ }
79
+ catch (err) {
80
+ sseControllers.delete(controller);
81
+ }
82
+ }
83
+ }
84
+ });
85
+ }
86
+ // Routes API
87
+ app.get("/proxy/health", (c) => {
88
+ const memory = process.memoryUsage();
89
+ return c.json({
90
+ status: "ok",
91
+ proxy: proxyClient ? proxyClient.getStatus() : (process.env.PROXY_URL ? "error" : "fallback"),
92
+ uptime: process.uptime(),
93
+ memory: {
94
+ rss: memory.rss,
95
+ heapUsed: memory.heapUsed,
96
+ },
97
+ ipc: process.connected ? "connected" : "disconnected",
98
+ timestamp: new Date().toISOString(),
99
+ });
100
+ });
101
+ app.get("/api/events", (c) => {
102
+ return streamSSE(c, async (stream) => {
103
+ sseControllers.add(stream);
104
+ stream.onAbort(() => {
105
+ sseControllers.delete(stream);
106
+ });
107
+ if (process.send) {
108
+ process.send({ type: "GET_INITIAL_STATE", timestamp: new Date().toISOString() });
109
+ }
110
+ await stream.writeSSE({ data: "connected", event: "ping" });
111
+ while (true) {
112
+ await stream.sleep(30000);
113
+ try {
114
+ await stream.writeSSE({ data: "ping", event: "ping" });
115
+ }
116
+ catch (e) {
117
+ sseControllers.delete(stream);
118
+ break;
119
+ }
120
+ }
121
+ });
122
+ });
123
+ app.post("/api/update-node", async (c) => {
124
+ const body = await c.req.json();
125
+ const { processName, nodeType, content } = body;
126
+ if (!processName || !nodeType || content === undefined) {
127
+ return c.json({ error: "Missing required fields" }, 400);
128
+ }
129
+ if (!process.send) {
130
+ return c.json({ error: "IPC not available" }, 500);
131
+ }
132
+ const correlationId = Math.random().toString(36).substring(7);
133
+ const message = {
134
+ type: "UPDATE_NODE",
135
+ correlationId,
136
+ data: { processName, nodeType, content },
137
+ timestamp: new Date().toISOString()
138
+ };
139
+ return new Promise((resolve) => {
140
+ const timeout = setTimeout(() => {
141
+ if (pendingRequests.has(correlationId)) {
142
+ pendingRequests.delete(correlationId);
143
+ resolve(c.json({ error: "Request timed out" }, 504));
144
+ }
145
+ }, 5000);
146
+ pendingRequests.set(correlationId, {
147
+ resolve: (msg) => {
148
+ clearTimeout(timeout);
149
+ if (msg.error)
150
+ resolve(c.json({ error: msg.error }, 400));
151
+ else
152
+ resolve(c.json({ success: true, process: msg.data?.process }));
153
+ },
154
+ reject: (err) => {
155
+ clearTimeout(timeout);
156
+ resolve(c.json({ error: err.message }, 500));
157
+ }
158
+ });
159
+ process.send(message);
160
+ });
161
+ });
162
+ // Service Statique
163
+ app.use("/assets/*", serveStatic({ root: path.relative(process.cwd(), absoluteStaticRoot) }));
164
+ // Fallback SPA
165
+ app.get("/*", (c, next) => {
166
+ const urlPath = c.req.path;
167
+ if (urlPath.startsWith('/api') || urlPath.startsWith('/proxy') || urlPath.startsWith('/assets')) {
168
+ return next();
169
+ }
170
+ try {
171
+ const indexPath = path.join(absoluteStaticRoot, "index.html");
172
+ if (fs.existsSync(indexPath)) {
173
+ const html = fs.readFileSync(indexPath, "utf8");
174
+ return c.html(html);
175
+ }
176
+ return c.text("GUI Build non trouvé à " + absoluteStaticRoot, 404);
177
+ }
178
+ catch (err) {
179
+ return next();
180
+ }
181
+ });
182
+ async function startServer() {
183
+ const fallbackPort = parseInt(process.env.FIXED_GUI_PORT || "4000", 10);
184
+ const proxyUrl = process.env.PROXY_URL;
185
+ const appPath = process.env.APP_PATH || "/psm-gui";
186
+ const appName = process.env.APP_NAME || "Project State Manager GUI";
187
+ let finalPort = fallbackPort;
188
+ let publicUrl = `http://localhost:${fallbackPort}`;
189
+ let isProxied = false;
190
+ if (proxyUrl) {
191
+ proxyClient = new ProxyClient(proxyUrl);
192
+ const result = await proxyClient.register({ path: appPath, name: appName }, fallbackPort);
193
+ isProxied = result.success;
194
+ finalPort = result.port;
195
+ if (isProxied)
196
+ publicUrl = result.url || `${proxyUrl}${appPath}`;
197
+ }
198
+ const cleanup = async () => {
199
+ if (proxyClient)
200
+ await proxyClient.unregister();
201
+ process.exit(0);
202
+ };
203
+ process.on("SIGTERM", cleanup);
204
+ process.on("SIGINT", cleanup);
205
+ setupIpcErrorHandlers();
206
+ serve({ fetch: app.fetch, port: finalPort }, (info) => {
207
+ console.error(`[DEBUG] [GUI-WORKER] Dashboard server listening on port ${info.port}`);
208
+ if (process.send) {
209
+ const message = {
210
+ type: "READY",
211
+ data: { port: info.port, isProxied, url: publicUrl },
212
+ timestamp: new Date().toISOString()
213
+ };
214
+ process.send(IPCMessageSchema.parse(message));
215
+ }
216
+ });
217
+ }
218
+ startServer().catch((err) => {
219
+ console.error(`[DEBUG] [GUI-WORKER] Failed to start: ${err.message}`);
220
+ process.exit(1);
221
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};