@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 +21 -0
- package/README.md +53 -0
- package/bin/ai-md.js +99 -0
- package/package.json +43 -0
- package/scripts/ensure-agent-tools.sh +114 -0
- package/scripts/link-project.sh +138 -0
- package/scripts/sync-config.sh +295 -0
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
|