/**
* `harn tool new` ported to .harn — see harn#2308 (W8).
*
* Scaffolds a Harn package that exports one custom tool. The dispatch
* shim in crates/harn-cli/src/commands/tool.rs resolves the destination
* directory and validates the package alias in Rust, then hands the
* pre-computed identifiers to this script via env vars. The script does
* the template render + file write loop, and the shim finishes by
* generating docs/api.md through `generate_package_docs_impl` (kept in
* Rust because it depends on the full package manifest pipeline).
*
* Inputs (set by the shim before dispatch):
* HARN_TOOL_NAME — package alias (validated upstream)
* HARN_TOOL_DEST — absolute path to destination directory
* HARN_TOOL_IDENT — sanitized Harn identifier derived from name
* HARN_TOOL_HANDLER — handler function name (handle_<ident>)
* HARN_TOOL_DESCRIPTION — description string (raw, unescaped)
* HARN_TOOL_HARN_RANGE — current Harn version range (e.g. ">=0.7,<0.8")
*/
fn __escape_basic_string(value: string) -> string {
// Order matters: replace the backslash first so subsequent inserted
// backslashes are not re-escaped. The remaining characters mirror the
// Rust scaffolder's basic-string escaping for the (deliberately
// restricted) inputs we accept — names are ASCII identifiers and the
// description is a single user-supplied line. Control characters are
// accepted as-is; the Rust dispatch shim rejects multi-line input
// before reaching this point.
var out = replace(value, "\\", "\\\\")
out = replace(out, "\"", "\\\"")
out = replace(out, "\n", "\\n")
out = replace(out, "\r", "\\r")
out = replace(out, "\t", "\\t")
return out
}
fn __harn_string_literal(value: string) -> string {
return "\"" + __escape_basic_string(value) + "\""
}
fn __toml_string_literal(value: string) -> string {
// TOML basic-string escaping uses the same escapes for the subset
// accepted here. Mirrors crates/harn-cli/src/package/manifest.rs.
return __harn_string_literal(value)
}
fn __strip_trailing_periods(value: string) -> string {
// substring(s, start, length) — second arg is length, not end index.
var out = value
while len(out) > 0 && ends_with(out, ".") {
out = substring(out, 0, len(out) - 1)
}
return out
}
fn __render_harn_toml(package_name: string, description: string, harn_range: string) -> string {
let pkg = __toml_string_literal(package_name)
let desc = __toml_string_literal(description)
let repo = __toml_string_literal("https://github.com/OWNER/" + package_name)
let prov = __toml_string_literal("https://github.com/OWNER/" + package_name + "/releases/tag/v0.1.0")
return "[package]\n"
+ "name = "
+ pkg
+ "\n"
+ "version = \"0.1.0\"\n"
+ "description = "
+ desc
+ "\n"
+ "license = \"MIT OR Apache-2.0\"\n"
+ "repository = "
+ repo
+ "\n"
+ "provenance = "
+ prov
+ "\n"
+ "harn = \""
+ harn_range
+ "\"\n"
+ "docs_url = \"docs/api.md\"\n"
+ "permissions = [\"tool:read_only\"]\n"
+ "\n"
+ "[exports]\n"
+ "tools = \"lib/tools.harn\"\n"
+ "\n"
+ "[[package.tools]]\n"
+ "name = "
+ pkg
+ "\n"
+ "module = \"lib/tools.harn\"\n"
+ "symbol = \"tools\"\n"
+ "description = "
+ desc
+ "\n"
+ "permissions = [\"tool:read_only\"]\n"
+ "\n"
+ "[package.tools.input_schema]\n"
+ "type = \"object\"\n"
+ "required = [\"text\"]\n"
+ "\n"
+ "[package.tools.input_schema.properties.text]\n"
+ "type = \"string\"\n"
+ "description = \"Text to echo.\"\n"
+ "\n"
+ "[package.tools.output_schema]\n"
+ "type = \"string\"\n"
+ "\n"
+ "[package.tools.annotations]\n"
+ "kind = \"read\"\n"
+ "side_effect_level = \"read_only\"\n"
+ "\n"
+ "[package.tools.annotations.arg_schema]\n"
+ "required = [\"text\"]\n"
+ "\n"
+ "[dependencies]\n"
}
fn __render_tools_harn(package_name: string, handler: string, description: string) -> string {
let tool_name = __harn_string_literal(package_name)
let desc_lit = __harn_string_literal(description)
// Mirrors the Rust handler's `description.trim_end_matches('.')` so
// the docstring summary reads naturally with the trailing period we
// add back in the rendered text.
let handler_doc = __strip_trailing_periods(description)
return "/** " + handler_doc + ". */\n"
+ "pub fn "
+ handler
+ "(args) {\n"
+ " let input = schema_expect(args, {\n"
+ " type: \"object\",\n"
+ " properties: {\n"
+ " text: {type: \"string\"}\n"
+ " },\n"
+ " required: [\"text\"]\n"
+ " })\n"
+ " return input.text\n"
+ "}\n"
+ "\n"
+ "/** Return a tool registry containing `"
+ package_name
+ "`. */\n"
+ "pub fn tools(registry = nil) {\n"
+ " var reg = registry ?? tool_registry()\n"
+ " reg = tool_define(reg, "
+ tool_name
+ ", "
+ desc_lit
+ ", {\n"
+ " parameters: {\n"
+ " text: {\n"
+ " type: \"string\",\n"
+ " description: \"Text to echo.\"\n"
+ " }\n"
+ " },\n"
+ " returns: {type: \"string\"},\n"
+ " annotations: {\n"
+ " kind: \"read\",\n"
+ " side_effect_level: \"read_only\",\n"
+ " arg_schema: {required: [\"text\"]}\n"
+ " },\n"
+ " handler: "
+ handler
+ "\n"
+ " })\n"
+ " return reg\n"
+ "}\n"
}
fn __render_main_harn(package_name: string) -> string {
let tool_name = __harn_string_literal(package_name)
return "import { agent_dispatch_tool_call } from \"std/agent/primitives\"\n"
+ "import { tools } from \"lib/tools\"\n"
+ "\n"
+ "pipeline default() {\n"
+ " let registry = tools()\n"
+ " var text = \"hello\"\n"
+ " if len(argv) > 0 {\n"
+ " text = argv[0]\n"
+ " }\n"
+ " let result = agent_dispatch_tool_call({\n"
+ " name: "
+ tool_name
+ ",\n"
+ " arguments: {text: text}\n"
+ " }, registry)\n"
+ " if !result.ok {\n"
+ " throw (result.error?.message ?? \"tool call failed\")\n"
+ " }\n"
+ " log(result.rendered_result)\n"
+ "}\n"
}
fn __render_test_harn(package_name: string, ident: string) -> string {
let tool_name = __harn_string_literal(package_name)
return "import { agent_dispatch_tool_call } from \"std/agent/primitives\"\n"
+ "import { tools } from \"../lib/tools\"\n"
+ "\n"
+ "pipeline test_"
+ ident
+ "_tool(task) {\n"
+ " let registry = tools()\n"
+ " let ok = agent_dispatch_tool_call({\n"
+ " name: "
+ tool_name
+ ",\n"
+ " arguments: {text: \"hello\"}\n"
+ " }, registry)\n"
+ " assert(ok.ok, ok.error?.message ?? \"tool call should pass\")\n"
+ " assert_eq(ok.rendered_result, \"hello\")\n"
+ "\n"
+ " let bad = agent_dispatch_tool_call({\n"
+ " name: "
+ tool_name
+ ",\n"
+ " arguments: {}\n"
+ " }, registry)\n"
+ " assert(!bad.ok, \"schema validation should reject missing text\")\n"
+ "}\n"
}
fn __render_readme(package_name: string, description: string) -> string {
return "# " + package_name + "\n"
+ "\n"
+ description
+ "\n"
+ "\n"
+ "## Develop\n"
+ "\n"
+ "```bash\n"
+ "harn test tests/\n"
+ "harn package check\n"
+ "harn package docs --check\n"
+ "harn package pack --dry-run\n"
+ "```\n"
+ "\n"
+ "## Install into another project\n"
+ "\n"
+ "```bash\n"
+ "harn add ../"
+ package_name
+ "\n"
+ "harn install\n"
+ "```\n"
+ "\n"
+ "Consumers import the stable registry builder with:\n"
+ "\n"
+ "```harn\n"
+ "import { tools } from \""
+ package_name
+ "/tools\"\n"
+ "```\n"
}
fn __render_workflow_yaml() -> string {
return "name: Harn tool package\n"
+ "\n"
+ "on:\n"
+ " pull_request:\n"
+ " push:\n"
+ " branches: [main]\n"
+ "\n"
+ "jobs:\n"
+ " package:\n"
+ " runs-on: ubuntu-latest\n"
+ " steps:\n"
+ " - uses: actions/checkout@v4\n"
+ " - uses: taiki-e/install-action@cargo-binstall\n"
+ " - run: cargo binstall harn-cli --no-confirm\n"
+ " - run: harn install --locked --offline || harn install\n"
+ " - run: harn test tests/\n"
+ " - run: harn package check\n"
+ " - run: harn package docs --check\n"
+ " - run: harn package pack --dry-run\n"
}
fn __write(harness: Harness, dest: string, rel: string, content: string) {
let path = path_join(dest, rel)
let parent = dirname(path)
if parent != "" && !harness.fs.exists(parent) {
try {
harness.fs.mkdir(parent)
} catch (e) {
harness.stdio.eprintln("failed to create " + parent + ": " + to_string(e))
exit(1)
}
}
try {
harness.fs.write_text(path, content)
} catch (e) {
harness.stdio.eprintln("failed to write " + path + ": " + to_string(e))
exit(1)
}
}
fn main(harness: Harness) {
let name = harness.env.get_or("HARN_TOOL_NAME", "")
let dest = harness.env.get_or("HARN_TOOL_DEST", "")
let ident = harness.env.get_or("HARN_TOOL_IDENT", "")
let handler = harness.env.get_or("HARN_TOOL_HANDLER", "")
let description = harness.env.get_or("HARN_TOOL_DESCRIPTION", "")
let harn_range = harness.env.get_or("HARN_TOOL_HARN_RANGE", "")
if name == "" || dest == "" || ident == "" || handler == "" || harn_range == "" {
harness.stdio
.eprintln("tool new: HARN_TOOL_{NAME,DEST,IDENT,HANDLER,HARN_RANGE} must be set")
exit(2)
}
__write(harness, dest, "harn.toml", __render_harn_toml(name, description, harn_range))
__write(harness, dest, "lib/tools.harn", __render_tools_harn(name, handler, description))
__write(harness, dest, "main.harn", __render_main_harn(name))
__write(harness, dest, "tests/test_tool.harn", __render_test_harn(name, ident))
__write(harness, dest, "README.md", __render_readme(name, description))
__write(harness, dest, "LICENSE", "MIT OR Apache-2.0\n")
__write(harness, dest, ".github/workflows/harn-package.yml", __render_workflow_yaml())
}