@dujavi/ai-md 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dujavi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # ai-md
2
+
3
+ Public CLI for syncing a **private** personal AI config directory (`~/.ai-md`) and installing agent CLIs.
4
+
5
+ | Layer | What | Where |
6
+ |-------|------|--------|
7
+ | **Public** | This package (`ai-md`) — sync/link/ensure-tools scripts only | npm + [github.com/dujavi/ai-md](https://github.com/dujavi/ai-md) |
8
+ | **Private** | Your skills, rules, projects | `~/.ai-md` git repo (e.g. `github.com/<you>/.ai-md`) |
9
+
10
+ No personal rules or skills ship in this package.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm i -g @dujavi/ai-md
16
+ # or one-shot:
17
+ npx @dujavi/ai-md --help
18
+ ```
19
+
20
+ ## New machine
21
+
22
+ ```bash
23
+ npm i -g @dujavi/ai-md
24
+ export AI_MD_REMOTE=https://github.com/<you>/.ai-md.git # your private content repo
25
+ ai-md install
26
+ ai-md ensure-tools # grok + quota-axi
27
+ ```
28
+
29
+ Defaults (override with env):
30
+
31
+ - `AI_MD_DIR` → `~/.ai-md`
32
+ - `AI_MD_REMOTE` → `https://github.com/dujavi/.ai-md.git`
33
+
34
+ ## Commands
35
+
36
+ ```bash
37
+ ai-md install
38
+ ai-md pull
39
+ ai-md push -m "why"
40
+ ai-md status
41
+ ai-md doctor --fix
42
+ ai-md ensure-tools # alias: tools
43
+ ai-md link-project --repo ~/my-app
44
+ ```
45
+
46
+ `install` / `pull` / `doctor` keep:
47
+
48
+ - `~/.cursor/skills` → `$AI_MD_DIR/skills`
49
+ - `~/.cursor/rules` → `$AI_MD_DIR/rules`
50
+
51
+ ## Agent-friendly
52
+
53
+ Non-interactive flags only (`--dry-run`, `--force`, `--fix`, `-m`). No prompts. Prefer `npx -y @dujavi/ai-md <cmd>` when not installed globally.
package/bin/ai-md.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawnSync } = require("child_process");
5
+ const os = require("os");
6
+ const path = require("path");
7
+
8
+ const scriptsDir = path.join(__dirname, "..", "scripts");
9
+ const home = os.homedir();
10
+
11
+ const env = {
12
+ ...process.env,
13
+ AI_MD_DIR:
14
+ process.env.AI_MD_DIR ||
15
+ process.env.CURSOR_MD_DIR ||
16
+ path.join(home, ".ai-md"),
17
+ AI_MD_REMOTE:
18
+ process.env.AI_MD_REMOTE ||
19
+ process.env.CURSOR_MD_REMOTE ||
20
+ "https://github.com/dujavi/.ai-md.git",
21
+ };
22
+
23
+ // Keep legacy env names in sync for older wrappers.
24
+ env.CURSOR_MD_DIR = env.AI_MD_DIR;
25
+ env.CURSOR_MD_REMOTE = env.AI_MD_REMOTE;
26
+
27
+ const COMMANDS = {
28
+ install: { script: "sync-config.sh", prefix: ["install"] },
29
+ pull: { script: "sync-config.sh", prefix: ["pull"] },
30
+ push: { script: "sync-config.sh", prefix: ["push"] },
31
+ status: { script: "sync-config.sh", prefix: ["status"] },
32
+ doctor: { script: "sync-config.sh", prefix: ["doctor"] },
33
+ "ensure-tools": { script: "ensure-agent-tools.sh", prefix: [] },
34
+ tools: { script: "ensure-agent-tools.sh", prefix: [] },
35
+ "link-project": { script: "link-project.sh", prefix: [] },
36
+ link: { script: "link-project.sh", prefix: [] },
37
+ };
38
+
39
+ function printHelp() {
40
+ console.log(`ai-md — sync private ~/.ai-md skills/rules; install agent CLIs
41
+
42
+ Usage:
43
+ ai-md <command> [options]
44
+ npx @dujavi/ai-md <command> [options]
45
+
46
+ Commands:
47
+ install Clone AI_MD_REMOTE → ~/.ai-md (if needed); link ~/.cursor/{skills,rules}
48
+ pull git pull private config; refresh symlinks
49
+ push commit + push private config (-m/--message)
50
+ status repo + symlink health
51
+ doctor diagnose; --fix repairs symlinks
52
+ ensure-tools install/update grok + quota-axi (alias: tools)
53
+ link-project link a repo .cursor/ → ~/.ai-md/projects/<name>/
54
+ help show this help
55
+
56
+ Environment:
57
+ AI_MD_DIR Private config dir (default: ~/.ai-md)
58
+ AI_MD_REMOTE Git remote if clone needed (default: https://github.com/dujavi/.ai-md.git)
59
+
60
+ Examples:
61
+ npm i -g @dujavi/ai-md
62
+ ai-md install
63
+ ai-md ensure-tools
64
+ ai-md pull
65
+ ai-md push -m "Add routing rule"
66
+ ai-md link-project --repo ~/presenter
67
+ ai-md doctor --fix
68
+
69
+ Private skills/rules live in each person's AI_MD_DIR repo.
70
+ This package ships tooling only — no personal content.
71
+ `);
72
+ }
73
+
74
+ const [cmd, ...args] = process.argv.slice(2);
75
+
76
+ if (!cmd || cmd === "help" || cmd === "-h" || cmd === "--help") {
77
+ printHelp();
78
+ process.exit(0);
79
+ }
80
+
81
+ const mapped = COMMANDS[cmd];
82
+ if (!mapped) {
83
+ console.error(`error: unknown command: ${cmd}`);
84
+ console.error(` ai-md --help`);
85
+ process.exit(2);
86
+ }
87
+
88
+ const scriptPath = path.join(scriptsDir, mapped.script);
89
+ const result = spawnSync("bash", [scriptPath, ...mapped.prefix, ...args], {
90
+ stdio: "inherit",
91
+ env,
92
+ });
93
+
94
+ if (result.error) {
95
+ console.error(`error: failed to run ${mapped.script}: ${result.error.message}`);
96
+ process.exit(1);
97
+ }
98
+
99
+ process.exit(result.status === null ? 1 : result.status);
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@dujavi/ai-md",
3
+ "version": "0.1.0",
4
+ "description": "CLI to sync private ~/.ai-md Cursor skills/rules and install agent tools (grok, quota-axi)",
5
+ "bin": {
6
+ "ai-md": "bin/ai-md.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "scripts/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "test": "node bin/ai-md.js --help"
19
+ },
20
+ "keywords": [
21
+ "cursor",
22
+ "ai",
23
+ "skills",
24
+ "rules",
25
+ "quota-axi",
26
+ "grok",
27
+ "cli",
28
+ "axi"
29
+ ],
30
+ "author": "dujavi",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/dujavi/ai-md.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/dujavi/ai-md/issues"
38
+ },
39
+ "homepage": "https://github.com/dujavi/ai-md#readme",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # Ensure Grok Build CLI + quota-axi are installed on this machine.
3
+ # Safe to re-run (idempotent). Does not print secrets.
4
+ set -euo pipefail
5
+
6
+ DRY_RUN=0
7
+
8
+ usage() {
9
+ cat <<'EOF'
10
+ Usage: ensure-agent-tools.sh [--dry-run]
11
+
12
+ Installs or updates:
13
+ - grok (Grok Build CLI) via https://x.ai/cli/install.sh
14
+ - quota-axi via volta (if present) or npm -g
15
+
16
+ Then prints version + auth/quota smoke checks (no secrets).
17
+ EOF
18
+ }
19
+
20
+ log() { printf '%s\n' "$*"; }
21
+ err() { printf 'error: %s\n' "$*" >&2; }
22
+ run() {
23
+ if [[ "$DRY_RUN" -eq 1 ]]; then
24
+ log "dry-run: $*"
25
+ else
26
+ "$@"
27
+ fi
28
+ }
29
+
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ --dry-run) DRY_RUN=1; shift ;;
33
+ -h|--help|help) usage; exit 0 ;;
34
+ *) err "unknown argument: $1"; usage; exit 1 ;;
35
+ esac
36
+ done
37
+
38
+ ensure_path_hint() {
39
+ local need=0
40
+ case ":$PATH:" in
41
+ *":$HOME/.local/bin:"*) ;;
42
+ *) need=1; log "note: add \$HOME/.local/bin to PATH" ;;
43
+ esac
44
+ case ":$PATH:" in
45
+ *":$HOME/.grok/bin:"*) ;;
46
+ *) need=1; log "note: add \$HOME/.grok/bin to PATH (grok installer usually does)" ;;
47
+ esac
48
+ return 0
49
+ }
50
+
51
+ install_grok() {
52
+ if command -v grok >/dev/null 2>&1; then
53
+ log "grok present: $(grok --version 2>/dev/null || echo unknown)"
54
+ log "Updating grok via official installer…"
55
+ else
56
+ log "Installing grok via official installer…"
57
+ fi
58
+ if [[ "$DRY_RUN" -eq 1 ]]; then
59
+ log "dry-run: curl -fsSL https://x.ai/cli/install.sh | bash"
60
+ return 0
61
+ fi
62
+ curl -fsSL https://x.ai/cli/install.sh | bash
63
+ }
64
+
65
+ install_quota_axi() {
66
+ if command -v quota-axi >/dev/null 2>&1; then
67
+ log "quota-axi present: $(quota-axi --version 2>/dev/null || echo unknown)"
68
+ else
69
+ log "Installing quota-axi…"
70
+ fi
71
+ if command -v volta >/dev/null 2>&1; then
72
+ run volta install quota-axi
73
+ elif command -v npm >/dev/null 2>&1; then
74
+ run npm install -g quota-axi
75
+ else
76
+ err "Need volta or npm to install quota-axi"
77
+ return 1
78
+ fi
79
+ }
80
+
81
+ smoke() {
82
+ log ""
83
+ log "=== smoke checks ==="
84
+ if command -v grok >/dev/null 2>&1; then
85
+ log "grok: $(command -v grok)"
86
+ grok --version 2>&1 || true
87
+ if [[ -f "$HOME/.grok/auth.json" ]]; then
88
+ log "grok auth: ~/.grok/auth.json present"
89
+ else
90
+ log "grok auth: missing — run \`grok\` (browser) or set XAI_API_KEY"
91
+ fi
92
+ else
93
+ err "grok not on PATH after install"
94
+ fi
95
+
96
+ if command -v quota-axi >/dev/null 2>&1; then
97
+ log "quota-axi: $(command -v quota-axi)"
98
+ quota-axi auth 2>&1 || true
99
+ quota-axi --provider grok 2>&1 || true
100
+ else
101
+ err "quota-axi not on PATH after install"
102
+ fi
103
+
104
+ if ! command -v sqlite3 >/dev/null 2>&1; then
105
+ log "note: sqlite3 not found — Cursor quota via quota-axi may fail (install sqlite3)"
106
+ fi
107
+ }
108
+
109
+ ensure_path_hint
110
+ install_grok
111
+ install_quota_axi
112
+ smoke
113
+ log ""
114
+ log "Done. Re-run anytime; pair with: ai-md install"
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bash
2
+ # Link a repo's .cursor/ to ~/.ai-md/projects/<name>/ (project rules/skills).
3
+ set -euo pipefail
4
+
5
+ REPO_MD="${AI_MD_DIR:-${CURSOR_MD_DIR:-$HOME/.ai-md}}"
6
+ DRY_RUN=0
7
+ FORCE=0
8
+ NAME=""
9
+ TARGET=""
10
+
11
+ usage() {
12
+ cat <<'EOF'
13
+ Usage: ai-md link-project --repo <path> [--name <id>] [options]
14
+
15
+ Point a repository's .cursor/ at ~/.ai-md/projects/<name>/ so project
16
+ rules and skills live in the private AI_MD_DIR repo (not committed to the app).
17
+
18
+ Options:
19
+ --repo <path> Repository root to link (required)
20
+ --name <id> Project id under projects/ (default: basename of --repo)
21
+ --force Replace an existing non-symlink .cursor/
22
+ --dry-run Print actions without changing anything
23
+ -h, --help Show this help
24
+
25
+ Examples:
26
+ ai-md link-project --repo ~/presenter
27
+ ai-md link-project --repo ~/code/app --name my-app
28
+ ai-md link --repo . --force
29
+ npx -y @dujavi/ai-md link-project --repo ~/presenter
30
+
31
+ After linking, add .cursor/ to the app repo's .gitignore.
32
+ EOF
33
+ }
34
+
35
+ log() { printf '%s\n' "$*"; }
36
+ err() { printf 'error: %s\n' "$*" >&2; }
37
+ run() {
38
+ if [[ "$DRY_RUN" -eq 1 ]]; then
39
+ log "dry-run: $*"
40
+ else
41
+ "$@"
42
+ fi
43
+ }
44
+
45
+ while [[ $# -gt 0 ]]; do
46
+ case "$1" in
47
+ --repo)
48
+ TARGET="${2:?--repo requires a path}"
49
+ shift 2
50
+ ;;
51
+ --name)
52
+ NAME="${2:?--name requires a value}"
53
+ shift 2
54
+ ;;
55
+ --force)
56
+ FORCE=1
57
+ shift
58
+ ;;
59
+ --dry-run)
60
+ DRY_RUN=1
61
+ shift
62
+ ;;
63
+ -h|--help|help)
64
+ usage
65
+ exit 0
66
+ ;;
67
+ *)
68
+ err "unknown argument: $1"
69
+ usage
70
+ exit 1
71
+ ;;
72
+ esac
73
+ done
74
+
75
+ if [[ -z "$TARGET" ]]; then
76
+ err "missing --repo <path>"
77
+ err " ai-md link-project --repo ~/presenter"
78
+ exit 1
79
+ fi
80
+
81
+ if [[ ! -d "$TARGET" ]]; then
82
+ err "not a directory: $TARGET"
83
+ exit 1
84
+ fi
85
+
86
+ TARGET="$(cd "$TARGET" && pwd)"
87
+ if [[ -z "$NAME" ]]; then
88
+ NAME="$(basename "$TARGET")"
89
+ fi
90
+
91
+ PROJECT_DIR="$REPO_MD/projects/$NAME"
92
+ LINK="$TARGET/.cursor"
93
+
94
+ if [[ ! -d "$REPO_MD/.git" ]]; then
95
+ err "$REPO_MD is not a git repo; run ai-md install first"
96
+ exit 1
97
+ fi
98
+
99
+ run mkdir -p "$PROJECT_DIR/rules" "$PROJECT_DIR/skills"
100
+
101
+ if [[ -L "$LINK" ]]; then
102
+ current="$(readlink "$LINK")"
103
+ if [[ "$current" == "$PROJECT_DIR" ]]; then
104
+ log "ok: $LINK → $PROJECT_DIR"
105
+ else
106
+ log "repair: $LINK (was → $current)"
107
+ run ln -sfn "$PROJECT_DIR" "$LINK"
108
+ fi
109
+ elif [[ -e "$LINK" ]]; then
110
+ if [[ "$FORCE" -ne 1 ]]; then
111
+ err "$LINK exists and is not a symlink (use --force after backing up)"
112
+ exit 1
113
+ fi
114
+ log "replace: $LINK (--force)"
115
+ run rm -rf "$LINK"
116
+ run ln -sfn "$PROJECT_DIR" "$LINK"
117
+ else
118
+ run ln -sfn "$PROJECT_DIR" "$LINK"
119
+ log "linked: $LINK → $PROJECT_DIR"
120
+ fi
121
+
122
+ gitignore="$TARGET/.gitignore"
123
+ if [[ -f "$gitignore" ]] && grep -qxF '.cursor/' "$gitignore" 2>/dev/null; then
124
+ log "ok: .cursor/ already in .gitignore"
125
+ elif [[ -f "$gitignore" ]]; then
126
+ if [[ "$DRY_RUN" -eq 1 ]]; then
127
+ log "dry-run: append .cursor/ to $gitignore"
128
+ else
129
+ printf '\n# Personal Cursor config (symlink to ~/.ai-md)\n.cursor/\n' >>"$gitignore"
130
+ log "appended .cursor/ to .gitignore"
131
+ fi
132
+ else
133
+ log "note: no .gitignore at $TARGET — add .cursor/ manually"
134
+ fi
135
+
136
+ log "Project config: $PROJECT_DIR"
137
+ log " rules: $(find "$PROJECT_DIR/rules" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')"
138
+ log " skills: $(find "$PROJECT_DIR/skills" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')"
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env bash
2
+ # Sync global personal Cursor skills/rules via ~/.ai-md (git + symlinks).
3
+ # Invoked by the public `ai-md` npm package. Private content stays in AI_MD_DIR.
4
+ set -euo pipefail
5
+
6
+ REPO="${AI_MD_DIR:-${CURSOR_MD_DIR:-$HOME/.ai-md}}"
7
+ REMOTE_URL="${AI_MD_REMOTE:-${CURSOR_MD_REMOTE:-https://github.com/dujavi/.ai-md.git}}"
8
+ DRY_RUN=0
9
+ FORCE=0
10
+ COMMIT_MSG="Update personal AI skills/rules"
11
+
12
+ usage() {
13
+ cat <<'EOF'
14
+ Usage: ai-md <command> [options]
15
+
16
+ Sync private ~/.ai-md skills and rules (git). Tooling-only package — no personal content.
17
+
18
+ Commands:
19
+ install Create/repair ~/.cursor/{skills,rules} symlinks into AI_MD_DIR
20
+ pull git pull, then refresh symlinks (run before editing)
21
+ push Commit dirty changes and git push (run after editing)
22
+ status Show repo state, symlink health, and rule/skill counts
23
+ doctor Diagnose problems; with --fix, repair symlinks
24
+
25
+ Also: ai-md ensure-tools (grok + quota-axi)
26
+
27
+ Options:
28
+ -m, --message <msg> Commit message for push (default: generic update)
29
+ --dry-run Print actions without changing anything
30
+ --force Replace existing non-symlink paths (install/doctor --fix)
31
+ -h, --help Show this help
32
+
33
+ Environment:
34
+ AI_MD_DIR Private config path (default: ~/.ai-md)
35
+ AI_MD_REMOTE Clone URL if repo missing (default: https://github.com/dujavi/.ai-md.git)
36
+
37
+ Examples:
38
+ ai-md install
39
+ ai-md pull
40
+ ai-md push -m "Add grafana rule"
41
+ ai-md status
42
+ ai-md doctor --fix
43
+ npx -y @dujavi/ai-md pull
44
+ EOF
45
+ }
46
+
47
+ log() { printf '%s\n' "$*"; }
48
+ err() { printf 'error: %s\n' "$*" >&2; }
49
+ run() {
50
+ if [[ "$DRY_RUN" -eq 1 ]]; then
51
+ log "dry-run: $*"
52
+ else
53
+ "$@"
54
+ fi
55
+ }
56
+
57
+ ensure_repo() {
58
+ if [[ ! -d "$REPO/.git" ]]; then
59
+ err "$REPO is not a git repo."
60
+ err " git clone $REMOTE_URL $REPO"
61
+ err " or: ai-md install"
62
+ exit 1
63
+ fi
64
+ }
65
+
66
+ clone_if_missing() {
67
+ if [[ -d "$REPO/.git" ]]; then
68
+ return 0
69
+ fi
70
+
71
+ # One-time migration from the old ~/.cursor-md layout.
72
+ local legacy="$HOME/.cursor-md"
73
+ if [[ "$REPO" == "$HOME/.ai-md" ]] && [[ -d "$legacy/.git" ]] && [[ ! -e "$REPO" ]]; then
74
+ log "Migrating $legacy → $REPO"
75
+ run mv "$legacy" "$REPO"
76
+ return 0
77
+ fi
78
+
79
+ if [[ -e "$REPO" ]] && [[ ! -d "$REPO" ]]; then
80
+ err "$REPO exists and is not a directory"
81
+ exit 1
82
+ fi
83
+ if [[ -d "$REPO" ]] && [[ -n "$(ls -A "$REPO" 2>/dev/null || true)" ]]; then
84
+ err "$REPO exists but is not a git repo (and is not empty)"
85
+ exit 1
86
+ fi
87
+ log "Cloning $REMOTE_URL → $REPO"
88
+ run git clone "$REMOTE_URL" "$REPO"
89
+ }
90
+
91
+ link_path() {
92
+ local target="$1"
93
+ local link="$2"
94
+ local link_dir
95
+
96
+ mkdir -p "$(dirname "$link")"
97
+ mkdir -p "$target"
98
+
99
+ if [[ -L "$link" ]]; then
100
+ local current
101
+ current="$(readlink "$link")"
102
+ if [[ "$current" == "$target" ]]; then
103
+ log "ok: $link → $target"
104
+ return 0
105
+ fi
106
+ log "repair: $link (was → $current)"
107
+ run ln -sfn "$target" "$link"
108
+ return 0
109
+ fi
110
+
111
+ if [[ -e "$link" ]]; then
112
+ if [[ "$FORCE" -ne 1 ]]; then
113
+ err "$link exists and is not a symlink (use --force to replace)"
114
+ err " expected → $target"
115
+ return 1
116
+ fi
117
+ log "replace: $link (was a real path; --force)"
118
+ run rm -rf "$link"
119
+ fi
120
+
121
+ link_dir="$(dirname "$link")"
122
+ run mkdir -p "$link_dir"
123
+ run ln -sfn "$target" "$link"
124
+ log "linked: $link → $target"
125
+ }
126
+
127
+ ensure_symlinks() {
128
+ local rc=0
129
+ link_path "$REPO/skills" "$HOME/.cursor/skills" || rc=1
130
+ link_path "$REPO/rules" "$HOME/.cursor/rules" || rc=1
131
+ return "$rc"
132
+ }
133
+
134
+ count_rules() {
135
+ find "$REPO/rules" -maxdepth 1 -type f \( -name '*.mdc' -o -name '*.md' \) 2>/dev/null | wc -l | tr -d ' '
136
+ }
137
+
138
+ count_skills() {
139
+ find "$REPO/skills" -mindepth 1 -maxdepth 1 -type d ! -name '.*' 2>/dev/null | wc -l | tr -d ' '
140
+ }
141
+
142
+ cmd_install() {
143
+ clone_if_missing
144
+ ensure_repo
145
+ ensure_symlinks
146
+ log "Install complete."
147
+ log " rules: $(count_rules)"
148
+ log " skills: $(count_skills)"
149
+ log "Next: ai-md ensure-tools # grok + quota-axi"
150
+ }
151
+
152
+ cmd_pull() {
153
+ ensure_repo
154
+ if [[ "$DRY_RUN" -eq 1 ]]; then
155
+ log "dry-run: git -C $REPO pull --rebase --autostash"
156
+ else
157
+ git -C "$REPO" pull --rebase --autostash
158
+ fi
159
+ ensure_symlinks
160
+ log "Pulled $REPO; symlinks refreshed."
161
+ }
162
+
163
+ cmd_push() {
164
+ ensure_repo
165
+ ensure_symlinks
166
+ if [[ "$DRY_RUN" -eq 1 ]]; then
167
+ git -C "$REPO" status -sb
168
+ log "dry-run: would add/commit/push with message: $COMMIT_MSG"
169
+ return 0
170
+ fi
171
+ git -C "$REPO" add -A
172
+ if git -C "$REPO" diff --cached --quiet; then
173
+ log "Nothing to commit in $REPO."
174
+ else
175
+ git -C "$REPO" commit -m "$COMMIT_MSG"
176
+ fi
177
+ git -C "$REPO" push
178
+ log "Pushed $REPO."
179
+ }
180
+
181
+ cmd_status() {
182
+ ensure_repo
183
+ log "Repo: $REPO"
184
+ log "Branch: $(git -C "$REPO" rev-parse --abbrev-ref HEAD)"
185
+ log "Remote: $(git -C "$REPO" remote get-url origin 2>/dev/null || echo '(none)')"
186
+ log ""
187
+ log "Global symlinks:"
188
+ for name in skills rules; do
189
+ local path="$HOME/.cursor/$name"
190
+ if [[ -L "$path" ]]; then
191
+ log " $path → $(readlink "$path")"
192
+ elif [[ -e "$path" ]]; then
193
+ log " $path (exists, NOT a symlink)"
194
+ else
195
+ log " $path (missing)"
196
+ fi
197
+ done
198
+ log ""
199
+ log "Counts: $(count_rules) rules, $(count_skills) skills"
200
+ log ""
201
+ git -C "$REPO" status -sb
202
+ }
203
+
204
+ cmd_doctor() {
205
+ ensure_repo
206
+ local problems=0
207
+
208
+ for name in skills rules; do
209
+ local path="$HOME/.cursor/$name"
210
+ local expected="$REPO/$name"
211
+ if [[ ! -L "$path" ]]; then
212
+ err "symlink missing or wrong type: $path"
213
+ problems=$((problems + 1))
214
+ elif [[ "$(readlink "$path")" != "$expected" ]]; then
215
+ err "symlink target wrong: $path → $(readlink "$path") (want $expected)"
216
+ problems=$((problems + 1))
217
+ fi
218
+ done
219
+
220
+ if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
221
+ err "not a git work tree: $REPO"
222
+ problems=$((problems + 1))
223
+ fi
224
+
225
+ if [[ "$problems" -eq 0 ]]; then
226
+ log "doctor: healthy ($(count_rules) rules, $(count_skills) skills)"
227
+ return 0
228
+ fi
229
+
230
+ log "doctor: $problems issue(s)"
231
+ if [[ "${1:-}" == "--fix" ]] || [[ "$FORCE" -eq 1 ]]; then
232
+ ensure_symlinks
233
+ log "doctor: attempted repair"
234
+ else
235
+ err "Re-run with: ai-md doctor --fix"
236
+ return 1
237
+ fi
238
+ }
239
+
240
+ # --- arg parse ---
241
+ CMD=""
242
+ FIX=0
243
+ while [[ $# -gt 0 ]]; do
244
+ case "$1" in
245
+ install|pull|push|status|doctor)
246
+ CMD="$1"
247
+ shift
248
+ ;;
249
+ -m|--message)
250
+ COMMIT_MSG="${2:?--message requires a value}"
251
+ shift 2
252
+ ;;
253
+ --dry-run)
254
+ DRY_RUN=1
255
+ shift
256
+ ;;
257
+ --force)
258
+ FORCE=1
259
+ shift
260
+ ;;
261
+ --fix)
262
+ FIX=1
263
+ FORCE=1
264
+ shift
265
+ ;;
266
+ -h|--help|help)
267
+ usage
268
+ exit 0
269
+ ;;
270
+ *)
271
+ err "unknown argument: $1"
272
+ usage
273
+ exit 1
274
+ ;;
275
+ esac
276
+ done
277
+
278
+ if [[ -z "$CMD" ]]; then
279
+ usage
280
+ exit 1
281
+ fi
282
+
283
+ case "$CMD" in
284
+ install) cmd_install ;;
285
+ pull) cmd_pull ;;
286
+ push) cmd_push ;;
287
+ status) cmd_status ;;
288
+ doctor)
289
+ if [[ "$FIX" -eq 1 ]]; then
290
+ cmd_doctor --fix
291
+ else
292
+ cmd_doctor
293
+ fi
294
+ ;;
295
+ esac