diff --git a/.reuse/dep5 b/.reuse/dep5 index 57a11df4..dbbe9bf1 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,7 +1,7 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: git-mind Upstream-Contact: James Ross -Source: https://github.com/neuroglyph/git-mind +Source: https://github.com/flyingrobots/git-mind Files: * Copyright: 2025-2026 James Ross diff --git a/CHANGELOG.md b/CHANGELOG.md index af4c5ebb..c329255b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -357,10 +357,10 @@ Complete rewrite from C23 to Node.js on `@git-stunts/git-warp`. - Docker-based CI/CD - All C-specific documentation -[3.1.0]: https://github.com/neuroglyph/git-mind/releases/tag/v3.1.0 -[3.0.0]: https://github.com/neuroglyph/git-mind/releases/tag/v3.0.0 -[2.0.0-alpha.5]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.5 -[2.0.0-alpha.4]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.4 -[2.0.0-alpha.3]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.3 -[2.0.0-alpha.2]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.2 -[2.0.0-alpha.0]: https://github.com/neuroglyph/git-mind/releases/tag/v2.0.0-alpha.0 +[3.1.0]: https://github.com/flyingrobots/git-mind/releases/tag/v3.1.0 +[3.0.0]: https://github.com/flyingrobots/git-mind/releases/tag/v3.0.0 +[2.0.0-alpha.5]: https://github.com/flyingrobots/git-mind/releases/tag/v2.0.0-alpha.5 +[2.0.0-alpha.4]: https://github.com/flyingrobots/git-mind/releases/tag/v2.0.0-alpha.4 +[2.0.0-alpha.3]: https://github.com/flyingrobots/git-mind/releases/tag/v2.0.0-alpha.3 +[2.0.0-alpha.2]: https://github.com/flyingrobots/git-mind/releases/tag/v2.0.0-alpha.2 +[2.0.0-alpha.0]: https://github.com/flyingrobots/git-mind/releases/tag/v2.0.0-alpha.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b14ab65..195b1ca5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Thanks for your interest in contributing. This document covers the essentials. ## Setup ```bash -git clone https://github.com/neuroglyph/git-mind.git +git clone https://github.com/flyingrobots/git-mind.git cd git-mind npm install npm test diff --git a/GUIDE.md b/GUIDE.md index 2f554db3..10215455 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -51,7 +51,7 @@ git-mind captures those relationships explicitly, so you can query them, visuali ### From source ```bash -git clone https://github.com/neuroglyph/git-mind.git +git clone https://github.com/flyingrobots/git-mind.git cd git-mind npm install ``` @@ -484,7 +484,7 @@ Status values are normalized on read — `Done`, `DONE`, `complete`, `finished` ### Custom views (programmatic) ```javascript -import { defineView, renderView, loadGraph } from '@neuroglyph/git-mind'; +import { defineView, renderView, loadGraph } from '@flyingrobots/git-mind'; defineView('my-view', (nodes, edges) => ({ nodes: nodes.filter(n => n.startsWith('feature:')), @@ -544,7 +544,7 @@ git mind import graph.yaml --dry-run ### Programmatic import ```javascript -import { importFile, loadGraph } from '@neuroglyph/git-mind'; +import { importFile, loadGraph } from '@flyingrobots/git-mind'; const graph = await loadGraph('.'); const result = await importFile(graph, 'graph.yaml', { dryRun: false }); @@ -586,7 +586,7 @@ Edges created from directives get a confidence of **0.8** — high, but flagged ### Processing commits programmatically ```javascript -import { processCommit, loadGraph } from '@neuroglyph/git-mind'; +import { processCommit, loadGraph } from '@flyingrobots/git-mind'; const graph = await loadGraph('.'); await processCommit(graph, { @@ -641,7 +641,7 @@ When you run `git mind at `: ### Programmatic usage ```javascript -import { loadGraph, getEpochForRef, computeStatus, getCurrentTick, recordEpoch } from '@neuroglyph/git-mind'; +import { loadGraph, getEpochForRef, computeStatus, getCurrentTick, recordEpoch } from '@flyingrobots/git-mind'; const graph = await loadGraph('.'); @@ -745,7 +745,7 @@ import { defineView, renderView, listViews, classifyStatus, // Hooks parseDirectives, processCommit, -} from '@neuroglyph/git-mind'; +} from '@flyingrobots/git-mind'; ``` ### Initialize and load diff --git a/bin/git-mind.js b/bin/git-mind.js index f76b6ce1..b3245f80 100755 --- a/bin/git-mind.js +++ b/bin/git-mind.js @@ -9,13 +9,29 @@ import { init, link, view, list, remove, nodes, status, at, importCmd, importMar import { parseDiffRefs, collectDiffPositionals } from '../src/diff.js'; import { createContext } from '../src/context-envelope.js'; import { registerBuiltinExtensions } from '../src/extension.js'; +import { VERSION, NAME } from '../src/version.js'; +import { createUpdateChecker, defaultPorts } from '../src/update-check.js'; const args = process.argv.slice(2); const command = args[0]; const cwd = process.cwd(); +const jsonMode = args.includes('--json'); + +// Wire update checker — real adapters in production, no-op when suppressed +const updateController = new AbortController(); +const checker = process.env.GIT_MIND_DISABLE_UPGRADE_CHECK + ? { getNotification: () => null, triggerCheck: () => {} } + : createUpdateChecker({ + ...defaultPorts(`https://registry.npmjs.org/${NAME}/latest`), + currentVersion: VERSION, + packageName: NAME, + }); +checker.triggerCheck(updateController.signal); function printUsage() { - console.log(`Usage: git mind [options] + console.log(`git-mind v${VERSION} + +Usage: git mind [options] Context flags (read commands: view, nodes, status, export, doctor): --at Show graph as-of a git ref (HEAD~N, branch, SHA) @@ -170,6 +186,12 @@ function extractPositionals(args) { return positionals; } +// Handle --version / -v before the command switch +if (command === '--version' || command === '-v') { + console.log(VERSION); + process.exit(0); +} + switch (command) { case 'init': await init(cwd); @@ -499,3 +521,12 @@ switch (command) { process.exitCode = command ? 1 : 0; break; } + +// Cancel background fetch — don't hold the process open +updateController.abort(); + +// Show update notification on stderr (never in --json mode) +if (!jsonMode) { + const note = checker.getNotification(); + if (note) process.stderr.write(note); +} diff --git a/docs/contracts/cli/at.schema.json b/docs/contracts/cli/at.schema.json index ae5f982e..eee9750b 100644 --- a/docs/contracts/cli/at.schema.json +++ b/docs/contracts/cli/at.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/at.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/at.schema.json", "title": "git-mind at --json", "description": "Time-travel output from `git mind at --json`", "type": "object", diff --git a/docs/contracts/cli/content-delete.schema.json b/docs/contracts/cli/content-delete.schema.json index 604137ab..427fce3f 100644 --- a/docs/contracts/cli/content-delete.schema.json +++ b/docs/contracts/cli/content-delete.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/content-delete.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/content-delete.schema.json", "title": "git-mind content delete --json", "description": "Content deletion result from `git mind content delete --json`", "type": "object", diff --git a/docs/contracts/cli/content-meta.schema.json b/docs/contracts/cli/content-meta.schema.json index 18329633..c8cc9844 100644 --- a/docs/contracts/cli/content-meta.schema.json +++ b/docs/contracts/cli/content-meta.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/content-meta.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/content-meta.schema.json", "title": "git-mind content meta --json", "description": "Content metadata result from `git mind content meta --json`", "type": "object", diff --git a/docs/contracts/cli/content-set.schema.json b/docs/contracts/cli/content-set.schema.json index 25f9e672..991aa8fb 100644 --- a/docs/contracts/cli/content-set.schema.json +++ b/docs/contracts/cli/content-set.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/content-set.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/content-set.schema.json", "title": "git-mind content set --json", "description": "Content attachment result from `git mind content set --json`", "type": "object", diff --git a/docs/contracts/cli/content-show.schema.json b/docs/contracts/cli/content-show.schema.json index 949fee4c..e58156ce 100644 --- a/docs/contracts/cli/content-show.schema.json +++ b/docs/contracts/cli/content-show.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/content-show.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/content-show.schema.json", "title": "git-mind content show --json", "description": "Content display result from `git mind content show --json`", "type": "object", diff --git a/docs/contracts/cli/diff.schema.json b/docs/contracts/cli/diff.schema.json index cbf87fd8..20500782 100644 --- a/docs/contracts/cli/diff.schema.json +++ b/docs/contracts/cli/diff.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/diff.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/diff.schema.json", "title": "git-mind diff --json", "description": "Graph diff result from `git mind diff .. --json`", "type": "object", diff --git a/docs/contracts/cli/doctor.schema.json b/docs/contracts/cli/doctor.schema.json index 6ac7ce5f..031e1da2 100644 --- a/docs/contracts/cli/doctor.schema.json +++ b/docs/contracts/cli/doctor.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/doctor.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/doctor.schema.json", "title": "git-mind doctor --json", "description": "Graph integrity check result from `git mind doctor --json`", "type": "object", diff --git a/docs/contracts/cli/export-data.schema.json b/docs/contracts/cli/export-data.schema.json index 2756edcb..b856735b 100644 --- a/docs/contracts/cli/export-data.schema.json +++ b/docs/contracts/cli/export-data.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/export-data.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/export-data.schema.json", "title": "git-mind export --json (stdout)", "description": "Graph data export from `git mind export --json` (stdout mode)", "type": "object", diff --git a/docs/contracts/cli/export-file.schema.json b/docs/contracts/cli/export-file.schema.json index f066077d..706d4833 100644 --- a/docs/contracts/cli/export-file.schema.json +++ b/docs/contracts/cli/export-file.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/export-file.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/export-file.schema.json", "title": "git-mind export --file --json", "description": "File export result from `git mind export --file --json`", "type": "object", diff --git a/docs/contracts/cli/extension-add.schema.json b/docs/contracts/cli/extension-add.schema.json index b8192095..28b359aa 100644 --- a/docs/contracts/cli/extension-add.schema.json +++ b/docs/contracts/cli/extension-add.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/extension-add.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/extension-add.schema.json", "title": "git-mind extension add --json", "description": "Extension registration result from `git mind extension add --json`", "type": "object", diff --git a/docs/contracts/cli/extension-list.schema.json b/docs/contracts/cli/extension-list.schema.json index b96721b9..4402714a 100644 --- a/docs/contracts/cli/extension-list.schema.json +++ b/docs/contracts/cli/extension-list.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/extension-list.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/extension-list.schema.json", "title": "git-mind extension list --json", "description": "Registered extensions from `git mind extension list --json`", "type": "object", diff --git a/docs/contracts/cli/extension-remove.schema.json b/docs/contracts/cli/extension-remove.schema.json index 65b124a4..5277e424 100644 --- a/docs/contracts/cli/extension-remove.schema.json +++ b/docs/contracts/cli/extension-remove.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/extension-remove.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/extension-remove.schema.json", "title": "git-mind extension remove --json", "description": "Extension removal result from `git mind extension remove --json`", "type": "object", diff --git a/docs/contracts/cli/extension-validate.schema.json b/docs/contracts/cli/extension-validate.schema.json index 21ce74fd..eccf43fa 100644 --- a/docs/contracts/cli/extension-validate.schema.json +++ b/docs/contracts/cli/extension-validate.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/extension-validate.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/extension-validate.schema.json", "title": "git-mind extension validate --json", "description": "Extension manifest validation result from `git mind extension validate --json`", "type": "object", diff --git a/docs/contracts/cli/import.schema.json b/docs/contracts/cli/import.schema.json index f97d2fce..44151a83 100644 --- a/docs/contracts/cli/import.schema.json +++ b/docs/contracts/cli/import.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/import.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/import.schema.json", "title": "git-mind import --json", "description": "Import result from `git mind import --json` or `git mind import --from-markdown --json`", "type": "object", diff --git a/docs/contracts/cli/merge.schema.json b/docs/contracts/cli/merge.schema.json index 7fa8c4e0..72751414 100644 --- a/docs/contracts/cli/merge.schema.json +++ b/docs/contracts/cli/merge.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/merge.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/merge.schema.json", "title": "git-mind merge --json", "description": "Merge result from `git mind merge --json`", "type": "object", diff --git a/docs/contracts/cli/node-detail.schema.json b/docs/contracts/cli/node-detail.schema.json index a82eb050..86948b21 100644 --- a/docs/contracts/cli/node-detail.schema.json +++ b/docs/contracts/cli/node-detail.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/node-detail.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/node-detail.schema.json", "title": "git-mind nodes --id --json", "description": "Single node detail output from `git mind nodes --id --json`", "type": "object", diff --git a/docs/contracts/cli/node-list.schema.json b/docs/contracts/cli/node-list.schema.json index b59c3142..91f4c690 100644 --- a/docs/contracts/cli/node-list.schema.json +++ b/docs/contracts/cli/node-list.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/node-list.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/node-list.schema.json", "title": "git-mind nodes --json", "description": "Node list output from `git mind nodes --json`", "type": "object", diff --git a/docs/contracts/cli/review-batch.schema.json b/docs/contracts/cli/review-batch.schema.json index 3a4f677a..5932608a 100644 --- a/docs/contracts/cli/review-batch.schema.json +++ b/docs/contracts/cli/review-batch.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/review-batch.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/review-batch.schema.json", "title": "git-mind review --batch --json", "description": "Batch review result from `git mind review --batch --json`", "type": "object", diff --git a/docs/contracts/cli/review-list.schema.json b/docs/contracts/cli/review-list.schema.json index 345f20e0..859d4c9d 100644 --- a/docs/contracts/cli/review-list.schema.json +++ b/docs/contracts/cli/review-list.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/review-list.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/review-list.schema.json", "title": "git-mind review --json", "description": "Pending review list from `git mind review --json`", "type": "object", diff --git a/docs/contracts/cli/set.schema.json b/docs/contracts/cli/set.schema.json index 14234b23..e3326fb2 100644 --- a/docs/contracts/cli/set.schema.json +++ b/docs/contracts/cli/set.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/set.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/set.schema.json", "title": "git-mind set --json", "description": "Output from `git mind set --json`", "type": "object", diff --git a/docs/contracts/cli/status.schema.json b/docs/contracts/cli/status.schema.json index d524b136..30bdba19 100644 --- a/docs/contracts/cli/status.schema.json +++ b/docs/contracts/cli/status.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/status.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/status.schema.json", "title": "git-mind status --json", "description": "Graph status dashboard output from `git mind status --json`", "type": "object", diff --git a/docs/contracts/cli/suggest.schema.json b/docs/contracts/cli/suggest.schema.json index bed3165c..9e43a1e6 100644 --- a/docs/contracts/cli/suggest.schema.json +++ b/docs/contracts/cli/suggest.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/suggest.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/suggest.schema.json", "title": "git-mind suggest --json", "description": "AI-powered edge suggestion result from `git mind suggest --json`", "type": "object", diff --git a/docs/contracts/cli/unset.schema.json b/docs/contracts/cli/unset.schema.json index ea1ce99a..03d52915 100644 --- a/docs/contracts/cli/unset.schema.json +++ b/docs/contracts/cli/unset.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/unset.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/unset.schema.json", "title": "git-mind unset --json", "description": "Output from `git mind unset --json`", "type": "object", diff --git a/docs/contracts/cli/view-lens.schema.json b/docs/contracts/cli/view-lens.schema.json index bf838391..383d978b 100644 --- a/docs/contracts/cli/view-lens.schema.json +++ b/docs/contracts/cli/view-lens.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/view-lens.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/view-lens.schema.json", "title": "git-mind view with lenses --json", "description": "View output with lens chaining from `git mind view :: --json`", "type": "object", diff --git a/docs/contracts/cli/view-progress.schema.json b/docs/contracts/cli/view-progress.schema.json index dcab49bd..81145d20 100644 --- a/docs/contracts/cli/view-progress.schema.json +++ b/docs/contracts/cli/view-progress.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/neuroglyph/git-mind/docs/contracts/cli/view-progress.schema.json", + "$id": "https://github.com/flyingrobots/git-mind/docs/contracts/cli/view-progress.schema.json", "title": "git-mind view progress --json", "description": "Progress view output from `git mind view progress --json`", "type": "object", diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..3a34b14b --- /dev/null +++ b/install.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# install.sh — Install @flyingrobots/git-mind globally +# Usage: +# ./install.sh npm install -g +# ./install.sh --prefix ~/.local install to ~/.local/bin +# curl -fsSL | sh pipe-friendly +set -e + +PACKAGE="@flyingrobots/git-mind" +MIN_NODE=22 +PREFIX="" + +# Parse arguments +while [ $# -gt 0 ]; do + case "$1" in + --prefix) + PREFIX="$2" + shift 2 + ;; + --prefix=*) + PREFIX="${1#*=}" + shift + ;; + -h|--help) + echo "Usage: $0 [--prefix ]" + echo "" + echo "Options:" + echo " --prefix Install to /bin instead of global npm" + echo "" + echo "Examples:" + echo " $0 # npm install -g" + echo " $0 --prefix ~/.local # install to ~/.local/bin" + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +# Check Node.js +if ! command -v node >/dev/null 2>&1; then + echo "Error: Node.js is not installed." >&2 + echo "Install Node.js >= ${MIN_NODE} from https://nodejs.org" >&2 + exit 1 +fi + +NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))") +if [ "$NODE_MAJOR" -lt "$MIN_NODE" ]; then + echo "Error: Node.js >= ${MIN_NODE} required (found v$(node -v))" >&2 + exit 1 +fi + +# Check npm +if ! command -v npm >/dev/null 2>&1; then + echo "Error: npm is not installed." >&2 + exit 1 +fi + +echo "Installing ${PACKAGE}..." + +if [ -n "$PREFIX" ]; then + # Expand ~ if present + PREFIX=$(eval echo "$PREFIX") + npm install -g --prefix "$PREFIX" "$PACKAGE" + + BIN_DIR="${PREFIX}/bin" + echo "" + echo "Installed to ${BIN_DIR}/git-mind" + echo "" + + # Check if BIN_DIR is in PATH + case ":$PATH:" in + *":${BIN_DIR}:"*) ;; + *) + echo "Add ${BIN_DIR} to your PATH:" + echo "" + echo " export PATH=\"${BIN_DIR}:\$PATH\"" + echo "" + echo "Add that line to your ~/.bashrc, ~/.zshrc, or ~/.profile" + ;; + esac +else + npm install -g "$PACKAGE" +fi + +echo "" +echo "Done! Run 'git mind --version' to verify." diff --git a/package-lock.json b/package-lock.json index 5ca4d88b..72249040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { - "name": "@neuroglyph/git-mind", + "name": "@flyingrobots/git-mind", "version": "4.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@neuroglyph/git-mind", + "name": "@flyingrobots/git-mind", "version": "4.0.1", "license": "Apache-2.0", "dependencies": { + "@git-stunts/alfred": "^0.10.3", "@git-stunts/git-warp": "^11.5.0", "@git-stunts/plumbing": "^2.8.0", "ajv": "^8.17.1", @@ -804,9 +805,9 @@ } }, "node_modules/@git-stunts/alfred": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@git-stunts/alfred/-/alfred-0.4.0.tgz", - "integrity": "sha512-80/W7x4pZYRyJB/AQs89aDbi+BcA7/N8G67zdJbKU7lbdrbbtglnY15/iSU66xvLD7/nFTTk9nGNhpw30h6QEQ==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@git-stunts/alfred/-/alfred-0.10.3.tgz", + "integrity": "sha512-dvy7Ej9Jyv9gPh4PtQuMfsZnUa7ycIwoFFnXLrQutRdoTTY4F4OOD2kcSJOs3w8UZhwOyLsHO7PcetaKB9g32w==", "license": "Apache-2.0", "engines": { "node": ">=20.0.0" @@ -831,15 +832,6 @@ "node": ">=22.0.0" } }, - "node_modules/@git-stunts/git-cas/node_modules/@git-stunts/alfred": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@git-stunts/alfred/-/alfred-0.10.3.tgz", - "integrity": "sha512-dvy7Ej9Jyv9gPh4PtQuMfsZnUa7ycIwoFFnXLrQutRdoTTY4F4OOD2kcSJOs3w8UZhwOyLsHO7PcetaKB9g32w==", - "license": "Apache-2.0", - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@git-stunts/git-warp": { "version": "11.5.0", "resolved": "https://registry.npmjs.org/@git-stunts/git-warp/-/git-warp-11.5.0.tgz", @@ -869,6 +861,15 @@ "node": ">=22.0.0" } }, + "node_modules/@git-stunts/git-warp/node_modules/@git-stunts/alfred": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@git-stunts/alfred/-/alfred-0.4.0.tgz", + "integrity": "sha512-80/W7x4pZYRyJB/AQs89aDbi+BcA7/N8G67zdJbKU7lbdrbbtglnY15/iSU66xvLD7/nFTTk9nGNhpw30h6QEQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@git-stunts/plumbing": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@git-stunts/plumbing/-/plumbing-2.8.0.tgz", diff --git a/package.json b/package.json index 0257fb6f..135f591e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@neuroglyph/git-mind", + "name": "@flyingrobots/git-mind", "version": "4.0.1", "description": "A project knowledge graph tool built on git-warp", "type": "module", @@ -7,25 +7,40 @@ "author": "James Ross ", "repository": { "type": "git", - "url": "https://github.com/neuroglyph/git-mind.git" + "url": "https://github.com/flyingrobots/git-mind.git" + }, + "homepage": "https://github.com/flyingrobots/git-mind#readme", + "bugs": { + "url": "https://github.com/flyingrobots/git-mind/issues" }, "bin": { "git-mind": "bin/git-mind.js" }, "exports": { - ".": { - "types": "./src/index.d.ts", - "default": "./src/index.js" - } + ".": "./src/index.js" + }, + "files": [ + "bin/", + "src/", + "docs/contracts/extension-manifest.schema.json", + "extensions/", + "LICENSE", + "NOTICE" + ], + "publishConfig": { + "access": "public" }, "scripts": { - "test": "vitest run", + "test": "vitest run --exclude test/contracts.integration.test.js --exclude test/content.test.js --exclude test/version.test.js", + "test:integration": "docker build -f test/Dockerfile -t git-mind-integration . && docker run --rm git-mind-integration", + "test:all": "npm test && npm run test:integration", "test:watch": "vitest", "test:coverage": "vitest run --coverage --exclude test/contracts.integration.test.js --exclude test/content.test.js --exclude test/version.test.js", "lint": "eslint src/ bin/", "format": "prettier --write 'src/**/*.js' 'bin/**/*.js'" }, "dependencies": { + "@git-stunts/alfred": "^0.10.3", "@git-stunts/git-warp": "^11.5.0", "@git-stunts/plumbing": "^2.8.0", "ajv": "^8.17.1", diff --git a/src/format-pr.js b/src/format-pr.js index 6bd9a91b..591be716 100644 --- a/src/format-pr.js +++ b/src/format-pr.js @@ -47,7 +47,7 @@ export function formatSuggestionsAsMarkdown(suggestions) { lines.push(''); lines.push(''); lines.push('---'); - lines.push('*Posted by [git-mind](https://github.com/neuroglyph/git-mind)*'); + lines.push('*Posted by [git-mind](https://github.com/flyingrobots/git-mind)*'); return lines.join('\n'); } diff --git a/src/index.js b/src/index.js index 9a910e39..c3434166 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ /** - * @module @neuroglyph/git-mind + * @module @flyingrobots/git-mind * Public API for git-mind — a project knowledge graph tool built on git-warp. */ @@ -51,3 +51,4 @@ export { export { writeContent, readContent, getContentMeta, hasContent, deleteContent, } from './content.js'; +export { VERSION, NAME } from './version.js'; diff --git a/src/update-check.js b/src/update-check.js new file mode 100644 index 00000000..52cf1b54 --- /dev/null +++ b/src/update-check.js @@ -0,0 +1,163 @@ +/** + * @module update-check + * Non-blocking update check against the npm registry. + * + * Hexagonal design: + * - createUpdateChecker(ports) — pure domain logic, no I/O at module level + * - Ports: fetchLatest, readCache, writeCache (injected) + * - defaultPorts() — real adapters: fs for cache, fetch + @git-stunts/alfred for registry + * - Tests inject fakes; CLI wires real adapters + * + * Flow: + * - triggerCheck(signal?) fires a background fetch (never awaited) + * - getNotification() reads the PREVIOUS run's cache (sync) + * - Cache lives at ~/.gitmind/update-check.json with 24h TTL + */ + +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { homedir } from 'node:os'; + +const CACHE_DIR = join(homedir(), '.gitmind'); +const CACHE_FILE = join(CACHE_DIR, 'update-check.json'); +const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +const DEFAULT_FETCH_TIMEOUT_MS = 3000; + +// ── Pure domain logic (no I/O) ────────────────────────────────── + +/** + * Compare two semver strings. Returns true if remote > local. + * Naive comparison — handles major.minor.patch only. + * @param {string} local + * @param {string} remote + * @returns {boolean} + */ +export function isNewer(local, remote) { + const parse = (v) => v.replace(/^v/, '').split('.').map(Number); + const l = parse(local); + const r = parse(remote); + for (let i = 0; i < 3; i++) { + if ((r[i] || 0) > (l[i] || 0)) return true; + if ((r[i] || 0) < (l[i] || 0)) return false; + } + return false; +} + +/** + * Format the update notification banner. + * @param {string} latest + * @param {string} currentVersion + * @param {string} packageName + * @returns {string} + */ +export function formatNotification(latest, currentVersion, packageName) { + return `\n Update available: ${currentVersion} → ${latest}\n Run \`npm install -g ${packageName}\` to update\n`; +} + +/** + * @typedef {object} UpdateCheckPorts + * @property {(signal?: AbortSignal) => Promise} fetchLatest Fetch latest version string from registry + * @property {() => {latest: string, checkedAt: number}|null} readCache Read cached check result (sync) + * @property {(data: {latest: string, checkedAt: number}) => void} writeCache Write check result to cache + * @property {string} currentVersion Current installed version + * @property {string} packageName Package name (for notification) + * @property {number} [cacheTtlMs] Cache TTL in ms (default 24h) + */ + +/** + * Create an update checker service. + * @param {UpdateCheckPorts} ports + * @returns {{ getNotification: () => string|null, triggerCheck: (signal?: AbortSignal) => void }} + */ +export function createUpdateChecker(ports) { + const { fetchLatest, readCache, writeCache, currentVersion, packageName, + cacheTtlMs = DEFAULT_CACHE_TTL_MS } = ports; + + function getNotification() { + try { + const data = readCache(); + if (!data?.latest || !isNewer(currentVersion, data.latest)) return null; + return formatNotification(data.latest, currentVersion, packageName); + } catch { + return null; + } + } + + function triggerCheck(signal) { + _doCheck(signal).catch(() => {}); + } + + async function _doCheck(signal) { + // Skip if cache is fresh + try { + const data = readCache(); + if (data && Date.now() - data.checkedAt < cacheTtlMs) return; + } catch { + // No cache or corrupt — proceed with fetch + } + + const latest = await fetchLatest(signal); + if (!latest) return; + writeCache({ latest, checkedAt: Date.now() }); + } + + return { getNotification, triggerCheck }; +} + +// ── Default adapters (real I/O) ───────────────────────────────── + +/** + * Build real adapters for production use. + * Alfred is lazy-loaded only when a fetch is needed. + * @param {string} registryUrl npm registry URL for the package + * @param {object} [opts] + * @param {number} [opts.timeoutMs] Fetch timeout (default 3000) + * @returns {Pick} + */ +export function defaultPorts(registryUrl, opts = {}) { + const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS } = opts; + + return { + readCache() { + try { + return JSON.parse(readFileSync(CACHE_FILE, 'utf-8')); + } catch { + return null; + } + }, + + writeCache(data) { + mkdirSync(CACHE_DIR, { recursive: true }); + writeFileSync(CACHE_FILE, JSON.stringify(data)); + }, + + async fetchLatest(signal) { + // Lazy-load Alfred on first fetch + const { Policy } = await import('@git-stunts/alfred'); + + // External signal goes on retry options for cancellation; + // timeout's internal signal is passed to fn for fetch abort. + const registryPolicy = Policy.timeout(timeoutMs) + .wrap(Policy.retry({ + retries: 1, + delay: 200, + backoff: 'exponential', + jitter: 'decorrelated', + signal, + })); + + const body = await registryPolicy.execute( + (timeoutSignal) => + fetch(registryUrl, { + signal: timeoutSignal, + headers: { Accept: 'application/json' }, + }).then((res) => { + if (!res.ok) throw new Error(`registry ${res.status}`); + return res.json(); + }), + ); + + return body.version || null; + }, + }; +} diff --git a/src/version.js b/src/version.js new file mode 100644 index 00000000..4f3f929c --- /dev/null +++ b/src/version.js @@ -0,0 +1,16 @@ +/** + * @module version + * Reads package version and name from package.json. + */ + +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url)); +const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + +/** @type {string} Semver version string */ +export const VERSION = pkg.version; + +/** @type {string} Package name (scoped) */ +export const NAME = pkg.name; diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 00000000..0dc31707 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,27 @@ +FROM node:22-slim + +RUN apt-get update && apt-get install -y git python3 make g++ && rm -rf /var/lib/apt/lists/* + +# Disable upgrade check — no network egress in test container +ENV GIT_MIND_DISABLE_UPGRADE_CHECK=1 +ENV NO_COLOR=1 + +WORKDIR /app + +# Install deps first (layer cache) +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy source +COPY bin/ bin/ +COPY src/ src/ +COPY test/ test/ +COPY docs/contracts/ docs/contracts/ +COPY extensions/ extensions/ + +# Git identity for tests that init repos +RUN git config --global user.email "test@test.com" && \ + git config --global user.name "Test" + +# Run only integration tests (files that spawn bin/git-mind.js) +CMD ["npx", "vitest", "run", "test/contracts.integration.test.js", "test/content.test.js", "test/version.test.js"] diff --git a/test/update-check.test.js b/test/update-check.test.js new file mode 100644 index 00000000..c9b37b4c --- /dev/null +++ b/test/update-check.test.js @@ -0,0 +1,213 @@ +/** + * @module test/update-check + * Tests for update-check: semver comparison, cache, notification, DI service. + * + * All tests use injected fakes — no network, no filesystem. + */ + +import { describe, it, expect } from 'vitest'; +import { isNewer, formatNotification, createUpdateChecker } from '../src/update-check.js'; + +// ── Helpers ───────────────────────────────────────────────────── + +/** Build a fake port set for createUpdateChecker. */ +function fakePorts(overrides = {}) { + return { + currentVersion: '1.0.0', + packageName: '@test/pkg', + cacheTtlMs: 1000, + readCache: () => null, + writeCache: () => {}, + fetchLatest: async () => null, + ...overrides, + }; +} + +// ── isNewer ───────────────────────────────────────────────────── + +describe('isNewer()', () => { + it('detects newer major version', () => { + expect(isNewer('1.0.0', '2.0.0')).toBe(true); + }); + + it('detects newer minor version', () => { + expect(isNewer('1.2.0', '1.3.0')).toBe(true); + }); + + it('detects newer patch version', () => { + expect(isNewer('1.2.3', '1.2.4')).toBe(true); + }); + + it('returns false for same version', () => { + expect(isNewer('1.2.3', '1.2.3')).toBe(false); + }); + + it('returns false for older version', () => { + expect(isNewer('2.0.0', '1.9.9')).toBe(false); + }); + + it('handles v-prefixed versions', () => { + expect(isNewer('v1.0.0', 'v2.0.0')).toBe(true); + }); + + it('returns false when local is newer', () => { + expect(isNewer('1.3.0', '1.2.0')).toBe(false); + }); +}); + +// ── formatNotification ────────────────────────────────────────── + +describe('formatNotification()', () => { + it('includes current and latest versions', () => { + const note = formatNotification('2.0.0', '1.0.0', '@test/pkg'); + expect(note).toContain('1.0.0'); + expect(note).toContain('2.0.0'); + }); + + it('includes install command with package name', () => { + const note = formatNotification('2.0.0', '1.0.0', '@test/pkg'); + expect(note).toContain('npm install -g @test/pkg'); + }); +}); + +// ── createUpdateChecker ───────────────────────────────────────── + +describe('createUpdateChecker()', () => { + describe('getNotification()', () => { + it('returns null when cache is empty', () => { + const checker = createUpdateChecker(fakePorts()); + expect(checker.getNotification()).toBeNull(); + }); + + it('returns null when cache has same version', () => { + const checker = createUpdateChecker(fakePorts({ + readCache: () => ({ latest: '1.0.0', checkedAt: Date.now() }), + })); + expect(checker.getNotification()).toBeNull(); + }); + + it('returns notification when cache has newer version', () => { + const checker = createUpdateChecker(fakePorts({ + readCache: () => ({ latest: '2.0.0', checkedAt: Date.now() }), + })); + const note = checker.getNotification(); + expect(note).toContain('2.0.0'); + expect(note).toContain('npm install -g @test/pkg'); + }); + + it('returns null when cache has older version', () => { + const checker = createUpdateChecker(fakePorts({ + readCache: () => ({ latest: '0.5.0', checkedAt: Date.now() }), + })); + expect(checker.getNotification()).toBeNull(); + }); + + it('returns null when readCache throws', () => { + const checker = createUpdateChecker(fakePorts({ + readCache: () => { throw new Error('corrupt'); }, + })); + expect(checker.getNotification()).toBeNull(); + }); + }); + + describe('triggerCheck()', () => { + it('fetches and writes cache when no cache exists', async () => { + let written = null; + const checker = createUpdateChecker(fakePorts({ + fetchLatest: async () => '2.0.0', + writeCache: (data) => { written = data; }, + })); + + checker.triggerCheck(); + // Wait for the fire-and-forget to settle + await new Promise((r) => setTimeout(r, 50)); + + expect(written).not.toBeNull(); + expect(written.latest).toBe('2.0.0'); + expect(written.checkedAt).toBeGreaterThan(0); + }); + + it('skips fetch when cache is fresh', async () => { + let fetched = false; + const checker = createUpdateChecker(fakePorts({ + readCache: () => ({ latest: '1.0.0', checkedAt: Date.now() }), + fetchLatest: async () => { fetched = true; return '2.0.0'; }, + })); + + checker.triggerCheck(); + await new Promise((r) => setTimeout(r, 50)); + + expect(fetched).toBe(false); + }); + + it('fetches when cache is stale', async () => { + let fetched = false; + const checker = createUpdateChecker(fakePorts({ + cacheTtlMs: 1000, + readCache: () => ({ latest: '1.0.0', checkedAt: Date.now() - 5000 }), + fetchLatest: async () => { fetched = true; return '2.0.0'; }, + })); + + checker.triggerCheck(); + await new Promise((r) => setTimeout(r, 50)); + + expect(fetched).toBe(true); + }); + + it('does not write cache when fetchLatest returns null', async () => { + let written = false; + const checker = createUpdateChecker(fakePorts({ + fetchLatest: async () => null, + writeCache: () => { written = true; }, + })); + + checker.triggerCheck(); + await new Promise((r) => setTimeout(r, 50)); + + expect(written).toBe(false); + }); + + it('silently swallows fetch errors', async () => { + const checker = createUpdateChecker(fakePorts({ + fetchLatest: async () => { throw new Error('network down'); }, + })); + + // Should not throw + checker.triggerCheck(); + await new Promise((r) => setTimeout(r, 50)); + }); + + it('passes signal to fetchLatest', async () => { + let receivedSignal = null; + const checker = createUpdateChecker(fakePorts({ + fetchLatest: async (signal) => { receivedSignal = signal; return '2.0.0'; }, + })); + + const controller = new AbortController(); + checker.triggerCheck(controller.signal); + await new Promise((r) => setTimeout(r, 50)); + + expect(receivedSignal).toBe(controller.signal); + }); + + it('respects abort signal', async () => { + let fetched = false; + const checker = createUpdateChecker(fakePorts({ + fetchLatest: async (signal) => { + // Simulate slow fetch + await new Promise((r) => setTimeout(r, 200)); + if (signal?.aborted) throw new Error('aborted'); + fetched = true; + return '2.0.0'; + }, + })); + + const controller = new AbortController(); + checker.triggerCheck(controller.signal); + controller.abort(); + await new Promise((r) => setTimeout(r, 300)); + + expect(fetched).toBe(false); + }); + }); +}); diff --git a/test/version.test.js b/test/version.test.js new file mode 100644 index 00000000..d6873fc5 --- /dev/null +++ b/test/version.test.js @@ -0,0 +1,37 @@ +/** + * @module test/version + * Tests for version module and --version CLI flag. + */ + +import { describe, it, expect } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import { join } from 'node:path'; +import { VERSION, NAME } from '../src/version.js'; + +const BIN = join(import.meta.dirname, '..', 'bin', 'git-mind.js'); + +describe('version module', () => { + it('VERSION is a valid semver string', () => { + expect(VERSION).toMatch(/^\d+\.\d+\.\d+/); + }); + + it('NAME is the scoped package name', () => { + expect(NAME).toBe('@flyingrobots/git-mind'); + }); +}); + +describe('--version CLI flag', () => { + it('prints version and exits with --version', () => { + const out = execFileSync(process.execPath, [BIN, '--version'], { + encoding: 'utf-8', + }).trim(); + expect(out).toBe(VERSION); + }); + + it('prints version and exits with -v', () => { + const out = execFileSync(process.execPath, [BIN, '-v'], { + encoding: 'utf-8', + }).trim(); + expect(out).toBe(VERSION); + }); +}); diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 00000000..0387a07f --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# uninstall.sh — Remove @flyingrobots/git-mind +# Usage: +# ./uninstall.sh npm uninstall -g +# ./uninstall.sh --prefix ~/.local remove from ~/.local +set -e + +PACKAGE="@flyingrobots/git-mind" +PREFIX="" +CLEAN_CACHE="" + +# Parse arguments +while [ $# -gt 0 ]; do + case "$1" in + --prefix) + PREFIX="$2" + shift 2 + ;; + --prefix=*) + PREFIX="${1#*=}" + shift + ;; + --clean-cache) + CLEAN_CACHE=1 + shift + ;; + -h|--help) + echo "Usage: $0 [--prefix ] [--clean-cache]" + echo "" + echo "Options:" + echo " --prefix Uninstall from instead of global npm" + echo " --clean-cache Also remove ~/.gitmind/ cache directory" + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +echo "Removing ${PACKAGE}..." + +if [ -n "$PREFIX" ]; then + PREFIX=$(eval echo "$PREFIX") + npm uninstall -g --prefix "$PREFIX" "$PACKAGE" +else + npm uninstall -g "$PACKAGE" +fi + +echo "Package removed." + +# Optionally clean cache +CACHE_DIR="${HOME}/.gitmind" +if [ -n "$CLEAN_CACHE" ] && [ -d "$CACHE_DIR" ]; then + rm -rf "$CACHE_DIR" + echo "Cache directory ${CACHE_DIR} removed." +elif [ -z "$CLEAN_CACHE" ] && [ -d "$CACHE_DIR" ]; then + echo "" + echo "Note: Cache directory ${CACHE_DIR} still exists." + echo "Run with --clean-cache to remove it." +fi + +echo "" +echo "Done!"