diff --git a/templates/space/functions/create_function.mjs b/space-functions/create-function.mjs similarity index 97% rename from templates/space/functions/create_function.mjs rename to space-functions/create-function.mjs index 8b219d6..5c0a91e 100644 --- a/templates/space/functions/create_function.mjs +++ b/space-functions/create-function.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; export const spec = { - name: 'create_function', + name: 'create-function', description: 'Create a new function on this remote by writing a function module.', parameters: { type: 'object', diff --git a/templates/space/functions/delete_file.mjs b/space-functions/delete-file.mjs similarity index 97% rename from templates/space/functions/delete_file.mjs rename to space-functions/delete-file.mjs index 8503a7c..c1f3f16 100644 --- a/templates/space/functions/delete_file.mjs +++ b/space-functions/delete-file.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; export const spec = { - name: 'delete_file', + name: 'delete-file', description: 'Delete a file (or directory) in the space.', parameters: { type: 'object', diff --git a/templates/space/functions/delete_function.mjs b/space-functions/delete-function.mjs similarity index 96% rename from templates/space/functions/delete_function.mjs rename to space-functions/delete-function.mjs index 5bb15fc..d9a4f6d 100644 --- a/templates/space/functions/delete_function.mjs +++ b/space-functions/delete-function.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; export const spec = { - name: 'delete_function', + name: 'delete-function', description: 'Delete a function module from this remote.', parameters: { type: 'object', diff --git a/templates/space/functions/list_files.mjs b/space-functions/list-files.mjs similarity index 96% rename from templates/space/functions/list_files.mjs rename to space-functions/list-files.mjs index 592d865..a134fc3 100644 --- a/templates/space/functions/list_files.mjs +++ b/space-functions/list-files.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; export const spec = { - name: 'list_files', + name: 'list-files', description: 'List every file in the space, as paths relative to the space directory.', parameters: { type: 'object', properties: {} }, }; diff --git a/templates/space/functions/read_file.mjs b/space-functions/read-file.mjs similarity index 96% rename from templates/space/functions/read_file.mjs rename to space-functions/read-file.mjs index 86c363b..61db296 100644 --- a/templates/space/functions/read_file.mjs +++ b/space-functions/read-file.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; export const spec = { - name: 'read_file', + name: 'read-file', description: 'Read the text content of a file in the space.', parameters: { type: 'object', diff --git a/templates/space/functions/write_file.mjs b/space-functions/write-file.mjs similarity index 97% rename from templates/space/functions/write_file.mjs rename to space-functions/write-file.mjs index 168e005..b8ef34d 100644 --- a/templates/space/functions/write_file.mjs +++ b/space-functions/write-file.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; export const spec = { - name: 'write_file', + name: 'write-file', description: 'Write text content to a file in the space, creating parent directories as needed.', parameters: { type: 'object', diff --git a/src/horde.mjs b/src/horde.mjs index d413ab2..c65e9b7 100644 --- a/src/horde.mjs +++ b/src/horde.mjs @@ -158,6 +158,6 @@ export class Horde { } if (packet.error) console.error('Hub error:', packet.error); - else console.warn('Ignored hub message:', raw); + else console.warn('Ignored hub message:', packet.action); } } \ No newline at end of file diff --git a/src/main.mjs b/src/main.mjs index ced212d..dc9451a 100644 --- a/src/main.mjs +++ b/src/main.mjs @@ -10,7 +10,7 @@ import path from 'node:path'; import { execFile } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { Horde } from './horde.mjs'; -import { Remote, Watcher } from './runtime.mjs'; +import { Remote, Watcher, Functions } from './runtime.mjs'; const APP_DIR = path.resolve(fileURLToPath(new URL('..', import.meta.url))); const TEMPLATES_DIR = path.join(APP_DIR, 'templates'); @@ -52,12 +52,21 @@ function addSpaceRemote(spaceName, dir) { const remoteName = `${CENTER_NAME}/${spaceName}`; let remote = REMOTES.get(remoteName); if (remote) return remote; + + // Resolve center functions for this space + let spaceConfig = APP_CONFIG.spaces?.[spaceName] || {}; + let centerFunctions = spaceConfig.centerFunctions; + if (centerFunctions == null) { + centerFunctions = CENTER ? CENTER.spaceFunctions.names() : []; + } + remote = new Remote({ horde: HORDE, name: remoteName, functionsDir: path.join(dir, '.remote', 'functions'), ctx: { dir, remoteName, spaceName, api: API }, - fallback: CENTER.functions, + centerFunctions, + fallback: CENTER && CENTER.spaceFunctions, }); REMOTES.set(remoteName, remote); return remote; @@ -70,14 +79,30 @@ async function initSpace(spaceName, dir) { throw new Error(`Space directory does not exist: ${absoluteDir}`); } + const remoteDir = path.join(absoluteDir, '.remote'); + if (!fs.existsSync(remoteDir)) { + fs.mkdirSync(remoteDir, { recursive: true }); + } + + // Create empty functions directory + const functionsDir = path.join(remoteDir, 'functions'); + if (!fs.existsSync(functionsDir)) { + fs.mkdirSync(functionsDir, { recursive: true }); + } + + // Get center function names for this space + const spaceConfig = APP_CONFIG.spaces?.[spaceName] || {}; + const centerFunctions = spaceConfig.centerFunctions || []; + + // Update config with center functions + APP_CONFIG = { ...APP_CONFIG, spaces: { ...APP_CONFIG.spaces, [spaceName]: { path: absoluteDir, centerFunctions } } }; + fs.writeFileSync(path.join(WORK_DIR, '.remote', 'config.json'), JSON.stringify(APP_CONFIG, null, 2) + '\n'); + if (REMOTES != null) { const remote = addSpaceRemote(spaceName, absoluteDir); if (!(await remote.register())) throw new Error('Failed to register space with the hub'); } - APP_CONFIG = { ...APP_CONFIG, spaces: { ...APP_CONFIG.spaces, [spaceName]: { path: absoluteDir } } }; - fs.writeFileSync(path.join(WORK_DIR, '.remote', 'config.json'), JSON.stringify(APP_CONFIG, null, 2) + '\n'); - console.log(`Initialized space '${spaceName}' at ${absoluteDir}`); } @@ -88,9 +113,6 @@ async function createSpace(spaceName, dir = null) { if (fs.existsSync(absoluteDir)) throw new Error(`Space directory already exists: ${absoluteDir}`); fs.mkdirSync(absoluteDir, { recursive: true }); - fs.cpSync(path.join(TEMPLATES_DIR, 'space'), path.join(absoluteDir, '.remote'), { recursive: true }); - fs.writeFileSync(path.join(absoluteDir, '.remote', 'config.json'), JSON.stringify({ name: spaceName }, null, 2) + '\n'); - await initSpace(spaceName, absoluteDir); } @@ -200,9 +222,10 @@ function init(dir = '.') { // --- run --- -function run() { +async function run() { const token = process.env.HORDE_TOKEN || APP_CONFIG.token || null; const spacesDir = path.join(WORK_DIR, 'spaces'); + const spaceFunctionsDir = path.join(WORK_DIR, 'space-functions'); console.log(`Running remote-center '${CENTER_NAME}' on ${HOST}, working path ${WORK_DIR}`); if (!token) console.warn('No token set in .remote/config.json'); @@ -222,6 +245,15 @@ function run() { }); REMOTES = new Map([[CENTER_NAME, CENTER]]); + // Load space functions + CENTER.spaceFunctions = new Functions(spaceFunctionsDir); + await CENTER.spaceFunctions.scan(); + + new Watcher(spaceFunctionsDir, async () => { + CENTER.spaceFunctions.scan(); + // TODO: Reconnect all space remotes + }).start(); + reconcileSpaces(); new Watcher(spacesDir, () => reconcileSpaces()).start(); HORDE.connect(); diff --git a/src/runtime.mjs b/src/runtime.mjs index aef1731..e6e9133 100644 --- a/src/runtime.mjs +++ b/src/runtime.mjs @@ -59,10 +59,18 @@ export class Functions { return [...this.map.values()].map((f) => f.spec); } + names() { + return [...this.map.keys()]; + } + has(name) { return this.map.has(name); } + spec(name) { + return this.map.get(name)?.spec; + } + async call(name, args, ctx) { const fn = this.map.get(name); if (!fn) throw new Error(`Unknown function: ${name}`); @@ -128,12 +136,13 @@ export class Watcher { // A named collection of functions in a directory, registered on the hub. export class Remote { - constructor({ horde, name, functionsDir, ctx = null, fallback = null }) { + constructor({ horde, name, functionsDir, ctx, centerFunctions = [], fallback }) { this.horde = horde; this.name = name; this.ctx = ctx; - this.fallback = fallback; // shared Functions instance (center functions) this.functions = new Functions(functionsDir); + this.centerFunctions = centerFunctions; + this.fallback = fallback; this.watcher = null; } @@ -141,17 +150,34 @@ export class Remote { async register() { await this.functions.scan(); try { + let functions = this.functions.specs(); + + + // Extend functions list with center functions + for (const fname of this.centerFunctions) { + // don't override local functions (should override?) + //if (this.functions.has(fname)) continue; + if (this.fallback && this.fallback.has(fname)) { + functions.push(this.fallback.spec(fname)); + } else { + console.error(`Remote ${this.name}: center function ${fname} not found`) + } + } + await this.horde.sendAction('remote-connect', { remotename: this.name, - functions: this.functions.specs(), + functions, }); - console.log(`Registered remote '${this.name}' with ${this.functions.map.size} function(s)`); + console.log(`Registered remote '${this.name}' with ${functions.length} function(s) (${this.functions.map.size} defined)`); } catch (err) { console.error(`Failed to register remote '${this.name}': ${err.message}`); return false; } if (!this.watcher) { - this.watcher = new Watcher(this.functions.dir, () => this.register()); + this.watcher = new Watcher(this.functions.dir, async () => { + await this.disconnect(); + await this.register() + }); this.watcher.start(); } return true;