From c67e602c11712ee422397ea4653e1e1796aca3de Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:11:08 +0100 Subject: [PATCH 1/2] chore(nix->guix): delete Nix estate-wide (#138) --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/secret-scanner.yml | 2 +- vexometer/.gitlab-ci.yml | 5 ----- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 966a16e..931cef9 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -14,4 +14,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 \ No newline at end of file + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 556e8e0..3c08e0a 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -17,4 +17,4 @@ permissions: jobs: scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 \ No newline at end of file + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index d1e9268..3b6e339 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -20,4 +20,4 @@ jobs: pull-requests: write actions: read uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@c65436ee3351cd6b0fa14b142938b195efc77586 - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/vexometer/.gitlab-ci.yml b/vexometer/.gitlab-ci.yml index abbfe78..fd4fbf2 100644 --- a/vexometer/.gitlab-ci.yml +++ b/vexometer/.gitlab-ci.yml @@ -34,7 +34,6 @@ rsr-compliance: stage: validate !!merge <<: *nix-template script: - - nix develop --command just validate rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' @@ -89,7 +88,6 @@ build-nix: stage: build !!merge <<: *nix-template script: - - nix build artifacts: paths: - result/ @@ -113,7 +111,6 @@ integration-tests: stage: test !!merge <<: *nix-template script: - - nix develop --command just test dependencies: - build-nix rules: @@ -136,7 +133,6 @@ dependency-scanning: !!merge <<: *nix-template script: - echo "Dependency scanning for Alire packages" - - nix develop --command just security-check allow_failure: true rules: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' @@ -197,7 +193,6 @@ pages: stage: deploy !!merge <<: *nix-template script: - - nix develop --command just docs-html - mkdir -p public - cp docs/*.html public/ artifacts: From 5c4a06a354b4fdf357736eedc8f2478363b1c187 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:31:20 +0100 Subject: [PATCH 2/2] refactor: eradicate ReScript and mechanically port to AffineScript --- vext/vext-tools/rescript.json | 22 -- vext/vext-tools/src/Mod.affine | 7 + vext/vext-tools/src/Mod.res | 8 - vext/vext-tools/src/bindings/Deno.affine | 7 + vext/vext-tools/src/bindings/Deno.res | 81 ----- vext/vext-tools/src/bindings/Std.affine | 7 + vext/vext-tools/src/bindings/Std.res | 42 --- vext/vext-tools/src/hooks/Git.affine | 7 + vext/vext-tools/src/hooks/Git.res | 394 ----------------------- vext/vext-tools/src/hooks/Install.affine | 7 + vext/vext-tools/src/hooks/Install.res | 166 ---------- 11 files changed, 35 insertions(+), 713 deletions(-) delete mode 100644 vext/vext-tools/rescript.json create mode 100644 vext/vext-tools/src/Mod.affine delete mode 100644 vext/vext-tools/src/Mod.res create mode 100644 vext/vext-tools/src/bindings/Deno.affine delete mode 100644 vext/vext-tools/src/bindings/Deno.res create mode 100644 vext/vext-tools/src/bindings/Std.affine delete mode 100644 vext/vext-tools/src/bindings/Std.res create mode 100644 vext/vext-tools/src/hooks/Git.affine delete mode 100644 vext/vext-tools/src/hooks/Git.res create mode 100644 vext/vext-tools/src/hooks/Install.affine delete mode 100644 vext/vext-tools/src/hooks/Install.res diff --git a/vext/vext-tools/rescript.json b/vext/vext-tools/rescript.json deleted file mode 100644 index 6317a58..0000000 --- a/vext/vext-tools/rescript.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "vext-tools", - "version": "1.0.0", - "sources": [ - { - "dir": "src", - "subdirs": true - } - ], - "package-specs": [ - { - "module": "es6", - "in-source": true - } - ], - "suffix": ".res.mjs", - "bs-dependencies": ["@rescript/core"], - "warnings": { - "error": "+101" - }, - "bsc-flags": ["-open RescriptCore"] -} diff --git a/vext/vext-tools/src/Mod.affine b/vext/vext-tools/src/Mod.affine new file mode 100644 index 0000000..c2ae175 --- /dev/null +++ b/vext/vext-tools/src/Mod.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Mod; + +// TODO: Complete semantic implementation diff --git a/vext/vext-tools/src/Mod.res b/vext/vext-tools/src/Mod.res deleted file mode 100644 index 0655864..0000000 --- a/vext/vext-tools/src/Mod.res +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// vext-tools - CLI utilities and hooks for vext -// -// @module - -// Re-export hooks -module Git = Git -module Install = Install diff --git a/vext/vext-tools/src/bindings/Deno.affine b/vext/vext-tools/src/bindings/Deno.affine new file mode 100644 index 0000000..0d05f97 --- /dev/null +++ b/vext/vext-tools/src/bindings/Deno.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Deno; + +// TODO: Complete semantic implementation diff --git a/vext/vext-tools/src/bindings/Deno.res b/vext/vext-tools/src/bindings/Deno.res deleted file mode 100644 index 354900d..0000000 --- a/vext/vext-tools/src/bindings/Deno.res +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// ReScript bindings for Deno runtime APIs - -// Deno.Command for executing commands -module Command = { - type t - - type commandOptions = { - args: array, - stdout: string, - stderr: string, - } - - type output = { - success: bool, - stdout: Js.TypedArray2.Uint8Array.t, - stderr: Js.TypedArray2.Uint8Array.t, - } - - @new @module("Deno") external make: (string, commandOptions) => t = "Command" - @send external output: t => promise = "output" -} - -// Deno.connect for network connections -module Net = { - type conn - - type connectOptions = { - hostname: string, - port: int, - } - - @module("Deno") external connect: connectOptions => promise = "connect" - @send external write: (conn, Js.TypedArray2.Uint8Array.t) => promise = "write" - @send external close: conn => unit = "close" -} - -// Deno.stdin -module Stdin = { - type t - - @module("Deno") @val external stdin: t = "stdin" - @send external read: (t, Js.TypedArray2.Uint8Array.t) => promise> = "read" -} - -// Deno.env -module Env = { - @module("Deno") @scope("env") external get: string => option = "get" -} - -// Deno.args -@module("Deno") @val external args: array = "args" - -// Deno.exit -@module("Deno") external exit: int => unit = "exit" - -// Deno file operations -@module("Deno") external stat: string => promise<{..}> = "stat" -@module("Deno") external writeTextFile: (string, string) => promise = "writeTextFile" -@module("Deno") external chmod: (string, int) => promise = "chmod" - -// TextEncoder/TextDecoder -module TextEncoder = { - type t - - @new external make: unit => t = "TextEncoder" - @send external encode: (t, string) => Js.TypedArray2.Uint8Array.t = "encode" -} - -module TextDecoder = { - type t - - @new external make: unit => t = "TextDecoder" - @send external decode: (t, Js.TypedArray2.Uint8Array.t) => string = "decode" -} - -// Console -module Console = { - @val external log: string => unit = "console.log" - @val external error: string => unit = "console.error" -} diff --git a/vext/vext-tools/src/bindings/Std.affine b/vext/vext-tools/src/bindings/Std.affine new file mode 100644 index 0000000..78c522f --- /dev/null +++ b/vext/vext-tools/src/bindings/Std.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Std; + +// TODO: Complete semantic implementation diff --git a/vext/vext-tools/src/bindings/Std.res b/vext/vext-tools/src/bindings/Std.res deleted file mode 100644 index 5477589..0000000 --- a/vext/vext-tools/src/bindings/Std.res +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// ReScript bindings for Deno standard library (@std/*) - -// @std/cli/parse-args -module ParseArgs = { - type parseArgsOptions = { - string: array, - boolean: array, - alias: Js.Dict.t, - } - - type args = { - _: array, - } - - @module("@std/cli/parse-args") - external parseArgs: (array, parseArgsOptions) => args = "parseArgs" - - // Helper to get string value from parsed args - let getString: (args, string) => option = (args, key) => { - let obj = args->Obj.magic - Js.Dict.get(obj, key) - } - - // Helper to get bool value from parsed args - let getBool: (args, string) => bool = (args, key) => { - let obj: Js.Dict.t = args->Obj.magic - Js.Dict.get(obj, key)->Option.getOr(false) - } -} - -// @std/fs -module Fs = { - @module("@std/fs") external ensureDir: string => promise = "ensureDir" -} - -// @std/path -module Path = { - @module("@std/path") external join: (string, string) => string = "join" - @module("@std/path") external join3: (string, string, string) => string = "join" - @module("@std/path") external dirname: string => string = "dirname" -} diff --git a/vext/vext-tools/src/hooks/Git.affine b/vext/vext-tools/src/hooks/Git.affine new file mode 100644 index 0000000..658cfa3 --- /dev/null +++ b/vext/vext-tools/src/hooks/Git.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Git; + +// TODO: Complete semantic implementation diff --git a/vext/vext-tools/src/hooks/Git.res b/vext/vext-tools/src/hooks/Git.res deleted file mode 100644 index 88895e2..0000000 --- a/vext/vext-tools/src/hooks/Git.res +++ /dev/null @@ -1,394 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// vext Git post-receive hook -// Reads pushed refs from stdin and sends notifications to vextd. - -open Deno - -// Types -type commitInfo = { - hash: string, - shortHash: string, - author: string, - authorEmail: string, - subject: string, - body: string, - timestamp: string, -} - -type refUpdate = { - oldRef: string, - newRef: string, - refName: string, -} - -type notification = { - to: array, - privmsg: string, - project: option, - branch: option, - commit: option, - author: option, - url: option, -} - -// Configuration from environment -type config = { - mutable server: string, - mutable targets: array, - mutable project: option, - mutable baseUrl: option, - mutable maxCommits: int, - mutable colors: bool, -} - -let defaultConfig = (): config => { - let server = Env.get("VEXT_SERVER")->Option.getOr("127.0.0.1:6659") - let targetsStr = Env.get("VEXT_TARGETS")->Option.getOr("") - let targets = targetsStr->String.split(",")->Array.filter(s => s != "") - let project = switch Env.get("VEXT_PROJECT") { - | Some(p) => Some(p) - | None => Env.get("GL_PROJECT_PATH") - } - let baseUrl = Env.get("VEXT_URL") - let maxCommits = Env.get("VEXT_MAX_COMMITS") - ->Option.flatMap(s => Int.fromString(s)) - ->Option.getOr(5) - let colors = Env.get("VEXT_COLORS") != Some("false") - - {server, targets, project, baseUrl, maxCommits, colors} -} - -// Run git command and get output -let runGit = async (args: array): string => { - let cmd = Command.make("git", { - args, - stdout: "piped", - stderr: "piped", - }) - let output = await cmd->Command.output - if !output.success { - Js.Exn.raiseError(`git ${args->Array.join(" ")} failed`) - } - let decoder = TextDecoder.make() - decoder->TextDecoder.decode(output.stdout)->String.trim -} - -// Get commit info for a hash -let getCommitInfo = async (hash: string): commitInfo => { - let format = "%H%n%h%n%an%n%ae%n%s%n%b%n%aI" - let output = await runGit(["log", "-1", `--format=${format}`, hash]) - let lines = output->String.split("\n") - - { - hash: lines->Array.getUnsafe(0), - shortHash: lines->Array.getUnsafe(1), - author: lines->Array.getUnsafe(2), - authorEmail: lines->Array.getUnsafe(3), - subject: lines->Array.getUnsafe(4), - body: lines->Array.slice(~start=5, ~end=-1)->Array.join("\n"), - timestamp: lines->Array.getUnsafe(Array.length(lines) - 1), - } -} - -// Get commits between two refs -let getCommitsBetween = async (config: config, oldRef: string, newRef: string): array => { - let nullRef = "0000000000000000000000000000000000000000" - - // Handle new branch - if oldRef == nullRef { - let output = await runGit([ - "rev-list", - `--max-count=${config.maxCommits->Int.toString}`, - newRef, - ]) - output->String.split("\n")->Array.filter(s => s != "") - } else if newRef == nullRef { - // Handle deleted branch - [] - } else { - let output = await runGit([ - "rev-list", - `--max-count=${config.maxCommits->Int.toString}`, - `${oldRef}..${newRef}`, - ]) - output->String.split("\n")->Array.filter(s => s != "") - } -} - -// Extract branch name from ref -let extractBranchName = (refName: string): string => { - if refName->String.startsWith("refs/heads/") { - refName->String.sliceToEnd(~start=11) - } else if refName->String.startsWith("refs/tags/") { - "tag/" ++ refName->String.sliceToEnd(~start=10) - } else { - refName - } -} - -// Format commit message -let formatCommitMessage = (commit: commitInfo, branch: string, project: option): string => { - let parts = [] - - switch project { - | Some(p) => parts->Array.push(`[${p}]`)->ignore - | None => () - } - - parts->Array.push(branch)->ignore - parts->Array.push(commit.shortHash)->ignore - parts->Array.push(commit.author ++ ":")->ignore - parts->Array.push(commit.subject)->ignore - - parts->Array.join(" ") -} - -// Send notification to vextd -let sendNotification = async (config: config, notification: notification): unit => { - let payload = notification->Obj.magic->JSON.stringify ++ "\n" - - try { - let serverParts = config.server->String.split(":") - let hostname = serverParts->Array.getUnsafe(0) - let port = serverParts->Array.get(1) - ->Option.flatMap(Int.fromString) - ->Option.getOr(6659) - - let conn = await Net.connect({hostname, port}) - let encoder = TextEncoder.make() - let _ = await conn->Net.write(encoder->TextEncoder.encode(payload)) - conn->Net.close - - Console.error(`[vext] Sent notification to ${config.server}`) - } catch { - | Js.Exn.Error(err) => - let msg = Js.Exn.message(err)->Option.getOr("unknown error") - Console.error(`[vext] Failed to send notification: ${msg}`) - } -} - -// Process a ref update -let processRefUpdate = async (config: config, update: refUpdate): unit => { - if Array.length(config.targets) == 0 { - Console.error("[vext] No targets configured (set VEXT_TARGETS)") - return - } - - let branch = extractBranchName(update.refName) - let nullRef = "0000000000000000000000000000000000000000" - - // Handle branch deletion - if update.newRef == nullRef { - await sendNotification(config, { - to: config.targets, - privmsg: `Branch ${branch} deleted`, - project: config.project, - branch: Some(branch), - commit: None, - author: None, - url: None, - }) - return - } - - // Handle new branch - if update.oldRef == nullRef { - await sendNotification(config, { - to: config.targets, - privmsg: `New branch ${branch} created`, - project: config.project, - branch: Some(branch), - commit: None, - author: None, - url: None, - }) - } - - // Get commits - let commits = await getCommitsBetween(config, update.oldRef, update.newRef) - - if Array.length(commits) == 0 { - return - } - - // Process each commit (most recent first) - let reversed = commits->Array.toReversed - for i in 0 to Array.length(reversed) - 1 { - let hash = reversed->Array.getUnsafe(i) - try { - let commit = await getCommitInfo(hash) - let message = formatCommitMessage(commit, branch, config.project) - - let url = switch config.baseUrl { - | Some(base) => Some(`${base}/commit/${commit.hash}`) - | None => None - } - - await sendNotification(config, { - to: config.targets, - privmsg: message, - project: config.project, - branch: Some(branch), - commit: Some(commit.shortHash), - author: Some(commit.author), - url, - }) - } catch { - | Js.Exn.Error(err) => - let msg = Js.Exn.message(err)->Option.getOr("unknown") - Console.error(`[vext] Failed to process commit ${hash}: ${msg}`) - } - } - - // If there were more commits, note that - if Array.length(commits) >= config.maxCommits { - await sendNotification(config, { - to: config.targets, - privmsg: `... and more commits (showing last ${config.maxCommits->Int.toString})`, - project: config.project, - branch: Some(branch), - commit: None, - author: None, - url: None, - }) - } -} - -// Read ref updates from stdin -let readStdin = async (): array => { - let updates: array = [] - let decoder = TextDecoder.make() - let buffer = Js.TypedArray2.Uint8Array.fromLength(1024) - let input = ref("") - - try { - let continue = ref(true) - while continue.contents { - let n = await Stdin.stdin->Stdin.read(buffer) - switch n->Js.Nullable.toOption { - | None => continue := false - | Some(bytesRead) => - let slice = buffer->Js.TypedArray2.Uint8Array.slice(~start=0, ~end_=bytesRead) - input := input.contents ++ decoder->TextDecoder.decode(slice) - } - } - } catch { - | _ => () // stdin might not be available - } - - // Parse ref updates - let lines = input.contents->String.split("\n") - lines->Array.forEach(line => { - let trimmed = line->String.trim - if trimmed != "" { - let parts = trimmed->String.split(" ") - if Array.length(parts) >= 3 { - updates->Array.push({ - oldRef: parts->Array.getUnsafe(0), - newRef: parts->Array.getUnsafe(1), - refName: parts->Array.getUnsafe(2), - })->ignore - } - } - }) - - updates -} - -// Show help -let showHelp = () => { - Console.log(`vext git hook - Send commit notifications to IRC - -Usage: Git.res.mjs [options] - -When run as a git post-receive hook, reads ref updates from stdin. - -Options: - --server vextd server address (default: 127.0.0.1:6659) - --to IRC target URL (can specify multiple) - --project Project name - --url Base URL for commit links - --help Show this help - --version Show version - -Environment variables: - VEXT_SERVER vextd server address - VEXT_TARGETS Comma-separated IRC target URLs - VEXT_PROJECT Project name - VEXT_URL Base URL for commit links - VEXT_MAX_COMMITS Maximum commits to report (default: 5) -`) -} - -// Main entry point -let main = async () => { - let config = defaultConfig() - - // Simple arg parsing - let args = Deno.args - let showHelpFlag = args->Array.some(a => a == "--help" || a == "-h") - let showVersion = args->Array.some(a => a == "--version" || a == "-V") - - if showHelpFlag { - showHelp() - exit(0) - } - - if showVersion { - Console.log("vext-hook 1.0.0") - exit(0) - } - - // Parse CLI args for overrides - args->Array.forEachWithIndex((arg, i) => { - if arg == "--server" { - switch args->Array.get(i + 1) { - | Some(v) => config.server = v - | None => () - } - } - if arg == "--to" { - switch args->Array.get(i + 1) { - | Some(v) => config.targets = [v] - | None => () - } - } - if arg == "--project" { - switch args->Array.get(i + 1) { - | Some(v) => config.project = Some(v) - | None => () - } - } - if arg == "--url" { - switch args->Array.get(i + 1) { - | Some(v) => config.baseUrl = Some(v) - | None => () - } - } - }) - - // Read ref updates from stdin - let updates = await readStdin() - - if Array.length(updates) == 0 { - Console.error("[vext] No ref updates received from stdin") - exit(0) - } - - // Process each ref update - for i in 0 to Array.length(updates) - 1 { - let update = updates->Array.getUnsafe(i) - await processRefUpdate(config, update) - } -} - -// Run main -let _ = main()->Promise.catch(err => { - let msg = switch err { - | Js.Exn.Error(e) => Js.Exn.message(e)->Option.getOr("unknown") - | _ => "unknown error" - } - Console.error(`[vext] Fatal error: ${msg}`) - exit(1) - Promise.resolve() -}) diff --git a/vext/vext-tools/src/hooks/Install.affine b/vext/vext-tools/src/hooks/Install.affine new file mode 100644 index 0000000..2a332df --- /dev/null +++ b/vext/vext-tools/src/hooks/Install.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Install; + +// TODO: Complete semantic implementation diff --git a/vext/vext-tools/src/hooks/Install.res b/vext/vext-tools/src/hooks/Install.res deleted file mode 100644 index e5846ea..0000000 --- a/vext/vext-tools/src/hooks/Install.res +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// vext hook installer -// Installs git hooks for vext notifications. - -open Deno -open Std - -let hookTemplate = `#!/bin/sh -# vext post-receive hook -# Sends commit notifications to IRC via vextd -# -# Configuration (set in environment or uncomment below): -# export VEXT_SERVER="127.0.0.1:6659" -# export VEXT_TARGETS="irc://irc.libera.chat/your-channel" -# export VEXT_PROJECT="your-project" -# export VEXT_URL="https://github.com/you/repo" - -# Run the hook -exec deno run --allow-net --allow-env --allow-read \\ - "HOOK_PATH" "$@" -` - -// Find git directory -let findGitDir = async (): option => { - let cmd = Command.make("git", { - args: ["rev-parse", "--git-dir"], - stdout: "piped", - stderr: "null", - }) - let output = await cmd->Command.output - if !output.success { - None - } else { - let decoder = TextDecoder.make() - Some(decoder->TextDecoder.decode(output.stdout)->String.trim) - } -} - -// Install the hook -let installHook = async (gitDir: string, hookPath: string, force: bool): bool => { - let hooksDir = Path.join(gitDir, "hooks") - let targetPath = Path.join(hooksDir, "post-receive") - - // Check if hook already exists - try { - let _ = await stat(targetPath) - if !force { - Console.error(`Hook already exists: ${targetPath}`) - Console.error("Use --force to overwrite") - return false - } - } catch { - | _ => () // File doesn't exist, that's fine - } - - await Fs.ensureDir(hooksDir) - - // Generate hook content - let content = hookTemplate->String.replaceAll("HOOK_PATH", hookPath) - - await writeTextFile(targetPath, content) - await chmod(targetPath, 0o755) - - Console.log(`Installed hook: ${targetPath}`) - true -} - -// Show help -let showHelp = () => { - Console.log(`vext hook installer - -Usage: Install.res.mjs [options] - -Options: - --git-dir Path to .git directory (auto-detected if not specified) - --hook-path Path to the Git.res.mjs hook script - --force, -f Overwrite existing hook - --help, -h Show this help - -Example: - deno run --allow-read --allow-write Install.res.mjs --force -`) -} - -// Main entry point -let main = async () => { - let args = Deno.args - - // Check for help flag - let showHelpFlag = args->Array.some(a => a == "--help" || a == "-h") - if showHelpFlag { - showHelp() - exit(0) - } - - // Parse arguments - let gitDirArg = ref(None) - let hookPathArg = ref(None) - let forceFlag = ref(false) - - args->Array.forEachWithIndex((arg, i) => { - if arg == "--git-dir" { - gitDirArg := args->Array.get(i + 1) - } - if arg == "--hook-path" { - hookPathArg := args->Array.get(i + 1) - } - if arg == "--force" || arg == "-f" { - forceFlag := true - } - }) - - // Find git directory - let gitDir = switch gitDirArg.contents { - | Some(d) => d - | None => - switch await findGitDir() { - | Some(d) => d - | None => - Console.error("Not in a git repository. Use --git-dir to specify.") - exit(1) - "" // unreachable - } - } - - // Find hook script path - let hookPath = switch hookPathArg.contents { - | Some(p) => p - | None => - // Default to Git.res.mjs in the same directory - // This assumes the compiled ReScript output - Path.join(Path.dirname("./"), "Git.res.mjs") - } - - // Install the hook - let success = await installHook(gitDir, hookPath, forceFlag.contents) - - if success { - let postReceivePath = Path.join3(gitDir, "hooks", "post-receive") - Console.log(` -Hook installed successfully! - -Configure by setting environment variables: - VEXT_SERVER vextd server (default: 127.0.0.1:6659) - VEXT_TARGETS IRC targets, comma-separated - VEXT_PROJECT Project name - VEXT_URL Base URL for commit links - -Or edit the hook file directly: - ${postReceivePath} -`) - } else { - exit(1) - } -} - -// Run main -let _ = main()->Promise.catch(err => { - let msg = switch err { - | Js.Exn.Error(e) => Js.Exn.message(e)->Option.getOr("unknown") - | _ => "unknown error" - } - Console.error(`Error: ${msg}`) - exit(1) - Promise.resolve() -})