Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 72 additions & 24 deletions src/goldfish.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3940,21 +3940,67 @@ find_gfproject_json (const char* gf_lib) {

static json
load_gfproject_config (const char* gf_lib) {
fs::path config_path= find_gfproject_json (gf_lib);
if (config_path.empty ()) {
// Find both gfproject.json files if they exist
std::error_code ec;
fs::path cwd= fs::current_path (ec);
fs::path local_path= cwd / "gfproject.json";
bool local_exists= !ec && fs::exists (local_path, ec) && fs::is_regular_file (local_path, ec);

fs::path lib_path= fs::path (gf_lib) / "gfproject.json";
bool lib_exists= fs::exists (lib_path, ec) && fs::is_regular_file (lib_path, ec);

// If neither exists, return empty object
if (!local_exists && !lib_exists) {
return json::object ();
}
try {
std::ifstream file (config_path);
if (!file.is_open ()) {
return json::object ();
}
json config;
file >> config;
return config;
} catch (...) {
return json::object ();

// Start with lib config as base (only tools section will be merged)
json merged= json::object ();
json lib_tools= json::object ();
if (lib_exists) {
try {
std::ifstream lib_file (lib_path);
if (lib_file.is_open ()) {
json lib_config;
lib_file >> lib_config;
if (lib_config.contains ("tools")) {
lib_tools= lib_config["tools"];
}
}
} catch (...) {
// Ignore parse errors for lib config
}
}

// Get local tools (local takes precedence)
json local_tools= json::object ();
if (local_exists) {
try {
std::ifstream local_file (local_path);
if (local_file.is_open ()) {
json local_config;
local_file >> local_config;
if (local_config.contains ("tools")) {
local_tools= local_config["tools"];
}
}
} catch (...) {
// Ignore parse errors for local config
}
}

// Merge tools: lib as base, local overrides (except "help" which is always internal)
json merged_tools= lib_tools;
for (auto& [key, value] : local_tools.items ()) {
// "help" tool is never merged - always use internal implementation
if (key == "help") {
continue;
}
merged_tools[key]= value;
}
merged["tools"]= merged_tools;

return merged;
}

static string
Expand Down Expand Up @@ -3989,25 +4035,17 @@ goldfish_run_tool (s7_scheme* sc, const char* gf_lib, const string& command,
json tool_config= config["tools"][command];

// Check if tool has implementation (organization and module)
// If not complete, fall back to internal implementation (return -1)
if (!tool_config.contains ("organization") || !tool_config.contains ("module")) {
cerr << "Error: Tool '" << command << "' is not fully implemented (missing organization or module)."
<< endl;
s7_close_output_port (sc, s7_current_error_port (sc));
s7_set_current_error_port (sc, old_port);
if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc);
return 1;
return -1; // Fall back to internal implementation
}

string org = tool_config["organization"];
string module = tool_config["module"];
string tool_root= find_tool_root_by_command (gf_lib, command);

if (tool_root.empty ()) {
cerr << "Error: tools/" << command << "/" << org << " directory not found." << endl;
s7_close_output_port (sc, s7_current_error_port (sc));
s7_set_current_error_port (sc, old_port);
if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc);
return 1;
return -1; // Fall back to internal implementation
}

s7_add_to_load_path (sc, tool_root.c_str ());
Expand Down Expand Up @@ -5315,8 +5353,9 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) {
}

// 处理动态注册的工具(从 gfproject.json 加载)
// 注意:"help" 命令始终使用内部实现,不从 gfproject.json 加载
json gfproject_config = load_gfproject_config (gf_lib);
if (gfproject_config.contains ("tools") && gfproject_config["tools"].contains (command)) {
if (command != "help" && gfproject_config.contains ("tools") && gfproject_config["tools"].contains (command)) {
int tool_ret = goldfish_run_tool (sc, gf_lib, command, errmsg, old_port, gc_loc);
if (tool_ret != -1) {
// Tool was found and executed (or failed with an error)
Expand All @@ -5325,6 +5364,15 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) {
// If tool_ret == -1, tool config exists but execution failed, fall through to check other commands
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading comment for tool_ret == -1

The comment says "tool config exists but execution failed", but -1 is now returned for several non-failure conditions: missing organization/module fields, or missing tool_root. A more accurate comment would be:

Suggested change
// If tool_ret == -1, tool config exists but execution failed, fall through to check other commands
// If tool_ret == -1, tool config is incomplete (missing org/module or tool root not found), fall back to built-in

}
Comment on lines 5357 to 5365
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 load_gfproject_config called twice per tool invocation

load_gfproject_config(gf_lib) is called here (line 5357) to check whether the command is present, and then called a second time at the very start of goldfish_run_tool (line 4030). Each call reads and parses both JSON files from disk. Passing the already-loaded config into goldfish_run_tool would avoid the duplicate I/O:

json gfproject_config = load_gfproject_config (gf_lib);
if (command != "help" && gfproject_config.contains ("tools") && gfproject_config["tools"].contains (command)) {
    int tool_ret = goldfish_run_tool (sc, gf_lib, command, gfproject_config, errmsg, old_port, gc_loc);

Then update goldfish_run_tool's signature to accept a const json& config parameter instead of loading it internally.


// 处理 help 命令(始终使用内部实现)
if (command == "help") {
display_help ();
s7_close_output_port (sc, s7_current_error_port (sc));
s7_set_current_error_port (sc, old_port);
if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc);
return 0;
}

// 处理 eval 子命令
if (command == "eval") {
if (argc < command_index + 1) {
Expand Down
84 changes: 70 additions & 14 deletions tools/help/liii/goldhelp.scm
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,79 @@
) ;export
(begin

(define (find-gfproject-path)
"Search for gfproject.json in current directory"
(define (find-gfproject-path kind)
"Search for gfproject.json - 'local or 'lib"
(let ((cwd (getcwd)))
(if cwd
(let ((test-path (path->string (path-join (path cwd) (path "gfproject.json")))))
(if (file-exists? test-path)
test-path
#f))
#f)))
(cond
((eq? kind 'local)
(if cwd
(let ((test-path (path->string (path-join (path cwd) (path "gfproject.json")))))
(if (file-exists? test-path)
test-path
#f))
#f))
((eq? kind 'lib)
;; First try GF_LIB environment variable
(let ((gf-lib (getenv "GF_LIB")))
(if gf-lib
(let ((lib-path (path->string (path-join (path gf-lib) (path "gfproject.json")))))
(if (file-exists? lib-path)
lib-path
#f))
;; Fallback: search from executable location upward
(let ((exe-path (executable)))
(if exe-path
(let search ((dir (path-dirname (path exe-path))))
(if dir
(let ((gfproject-path (path->string (path-join dir (path "gfproject.json")))))
(if (file-exists? gfproject-path)
gfproject-path
(search (path-dirname dir))))
#f))
#f)))))
Comment on lines +58 to +67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Recursive path-dirname search may not terminate

The fallback search walks up the directory tree by repeatedly calling (path-dirname dir). If path-dirname returns the same path for the filesystem root (e.g. "/""/") instead of returning #f, the search loop will spin forever. A termination guard comparing the current dir with its parent would make this safe:

(let search ((dir (path-dirname (path exe-path))))
  (if dir
      (let ((parent (path-dirname dir)))
        (if (or (not parent)
                (string=? (path->string dir) (path->string parent)))
            #f
            (let ((gfproject-path (path->string (path-join dir (path "gfproject.json")))))
              (if (file-exists? gfproject-path)
                  gfproject-path
                  (search parent)))))
      #f))

(else #f))))

(define (json-merge-tools lib-tools local-tools)
"Merge two tools JSON objects, local takes precedence (except 'help')"
(cond
((json-null? lib-tools)
(if (json-null? local-tools)
(make-json)
local-tools))
((json-null? local-tools)
lib-tools)
(else
;; Start with lib tools as base, then add local tools (except "help")
(let ((merged (make-json)))
;; Add lib tools (except "help")
(for-each
(lambda (key)
(when (not (string=? key "help"))
(json-set! merged key (json-ref lib-tools key))))
(json-keys lib-tools))
;; Add local tools (except "help"), overwriting lib values
(for-each
(lambda (key)
(when (not (string=? key "help"))
(json-set! merged key (json-ref local-tools key))))
(json-keys local-tools))
merged)))))

(define (load-gfproject)
"Load and parse gfproject.json, return tools object"
(let ((path (find-gfproject-path)))
(if path
(let ((content (path-read-text path)))
(string->json content))
(string->json "{}"))))
"Load and merge gfproject.json from local and lib, local takes precedence"
(let ((local-path (find-gfproject-path 'local))
(lib-path (find-gfproject-path 'lib)))
(let ((lib-config (if lib-path
(let ((content (path-read-text lib-path)))
(string->json content))
(make-json)))
(local-config (if local-path
(let ((content (path-read-text local-path)))
(string->json content))
(make-json))))
(let ((lib-tools (json-ref lib-config "tools"))
(local-tools (json-ref local-config "tools")))
(json-merge-tools lib-tools local-tools)))))
Comment on lines 96 to +110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 load-gfproject returns merged tools, not a full config object

json-merge-tools returns the tools object directly (e.g. {"help":{…},"fix":{…}}), so load-gfproject now returns that. But every caller still does (json-ref config "tools") on the return value — looking for a key named "tools" inside an object that has no such key. The result is json-null, causing display-help to show no dynamic tools and display-tool-help to always print "Unknown command: …".

The fix is to wrap the merged tools back into a config object:

(define (load-gfproject)
  "Load and merge gfproject.json from local and lib, local takes precedence"
  (let ((local-path (find-gfproject-path 'local))
        (lib-path   (find-gfproject-path 'lib)))
    (let ((lib-config   (if lib-path
                            (string->json (path-read-text lib-path))
                            (make-json)))
          (local-config (if local-path
                            (string->json (path-read-text local-path))
                            (make-json))))
      (let ((merged (make-json)))
        (json-set! merged "tools"
          (json-merge-tools (json-ref lib-config "tools")
                            (json-ref local-config "tools")))
        merged))))


(define (get-tool-description tools tool-name lang)
"Get description for a tool in specified language"
Expand Down
Loading