@dujavi/ai-md 0.6.0 → 0.6.1

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/README.md CHANGED
@@ -33,11 +33,15 @@ Cursor / Gemini / OpenCode may *also read* `~/.agents/skills`; enabling `agents`
33
33
  ```bash
34
34
  npm i -g @dujavi/ai-md
35
35
 
36
- # A) No private remote yet
36
+ # A) Existing private git repo (clone/sync first — never skeleton-before-sync)
37
+ ai-md setup --remote https://github.com/<you>/.ai-md.git
38
+
39
+ # B) No remote yet → local skeleton only
37
40
  ai-md init
38
41
 
39
- # B) Existing private git repo
40
- ai-md setup --remote https://github.com/<you>/.ai-md.git
42
+ # C) Optional auto-detect: if `gh` (or git github.user) identifies you
43
+ # AND github.com/<user>/.ai-md exists, init/setup use that remote.
44
+ # There is no hardcoded default remote.
41
45
 
42
46
  ai-md pull
43
47
  # edit shared/ or agents/<id>/ only
package/bin/ai-md.js CHANGED
@@ -21,6 +21,7 @@ const {
21
21
  buildAndLink,
22
22
  runBuild,
23
23
  runRescue,
24
+ bootstrapContent,
24
25
  seedSkeleton,
25
26
  initRepo,
26
27
  harnessList,
@@ -253,9 +254,9 @@ Unique sync targets: cursor, claude, gemini, opencode, copilot
253
254
  Shared ~/.agents/skills: agents (canonical); codex is an alias
254
255
 
255
256
  Commands:
256
- init Scaffold skeleton into ~/.ai-md (no remote required)
257
+ init Bootstrap ~/.ai-md (clone if remote known, else skeleton)
257
258
  seed-skeleton Add missing recommended files only
258
- setup Persist config + install (+ optional --init / --script)
259
+ setup Clone/sync if remote known; --init for skeleton; --script
259
260
  config Show config; config set --remote/--dir/--link-mode
260
261
  status Snapshot (default)
261
262
  doctor Diagnose; --fix rebuilds + relinks (installed AIs only)
@@ -371,6 +372,7 @@ function main() {
371
372
  agents: cfg.agents,
372
373
  defaultLinkMode: defaultLinkMode(),
373
374
  sources: cfg.sources,
375
+ remoteDetection: cfg.remoteDetection,
374
376
  },
375
377
  },
376
378
  json: opts.json,
@@ -379,32 +381,30 @@ function main() {
379
381
  break;
380
382
  }
381
383
  case "init": {
382
- const data = initRepo(cfg, {
384
+ const data = bootstrapContent(cfg, {
383
385
  noGit: opts.noGit,
384
386
  force: opts.force,
385
387
  dryRun: opts.dryRun,
388
+ forceLink: opts.forceLink,
389
+ agents: agentsOrDefault(opts, cfg),
386
390
  });
387
- writeMachineConfig(
388
- { dir: cfg.dir, agents: ["cursor"], linkMode: cfg.linkMode },
389
- process.env,
390
- { dryRun: opts.dryRun }
391
- );
392
- cfg = resolveConfig(process.env);
393
- let bl = null;
394
- if (!opts.dryRun) {
395
- bl = buildAndLink(cfg, {
396
- agents: ["cursor"],
397
- force: true,
398
- forceLink: opts.forceLink,
399
- });
400
- }
391
+ const help =
392
+ data.action === "cloned" ||
393
+ data.action === "synced" ||
394
+ data.action === "would_clone" ||
395
+ data.action === "would_replace_and_clone"
396
+ ? [
397
+ `Synced from ${data.remote || cfg.remote}`,
398
+ "Edit shared/ or agents/<id>/; then ai-md build && ai-md push -m \"…\"",
399
+ ]
400
+ : [
401
+ "Skeleton created (no remote).",
402
+ "Connect later: ai-md setup --remote https://github.com/<you>/.ai-md.git",
403
+ ];
401
404
  emit({
402
- data: { init: data, buildLink: bl },
405
+ data: { init: data },
403
406
  json: opts.json,
404
- help: [
405
- "Edit shared/ or agents/<id>/",
406
- "Later: ai-md config set --remote <url> && git -C ~/.ai-md push -u origin main",
407
- ],
407
+ help,
408
408
  });
409
409
  break;
410
410
  }
@@ -517,54 +517,63 @@ function main() {
517
517
  break;
518
518
  }
519
519
  case "setup": {
520
- if (opts.init && !opts.remote) {
521
- const data = initRepo(cfg, {
520
+ const remoteCfg = resolveConfig(process.env, {
521
+ dir: opts.dir || undefined,
522
+ remote: opts.remote || undefined,
523
+ });
524
+ const remote = opts.remote || remoteCfg.remote || null;
525
+
526
+ // Explicit skeleton path only when no remote is known
527
+ if (opts.init && !remote) {
528
+ const data = bootstrapContent(remoteCfg, {
522
529
  noGit: opts.noGit,
523
530
  force: opts.force,
524
531
  dryRun: opts.dryRun,
532
+ forceLink: opts.forceLink,
533
+ agents: agentsOrDefault(opts, remoteCfg),
525
534
  });
526
- writeMachineConfig(
527
- { dir: cfg.dir, agents: ["cursor"], linkMode: cfg.linkMode },
528
- process.env,
529
- { dryRun: opts.dryRun }
530
- );
531
- cfg = resolveConfig(process.env);
532
- const bl = opts.dryRun
533
- ? null
534
- : buildAndLink(cfg, {
535
- agents: agentsOrDefault(opts, cfg),
536
- force: true,
537
- forceLink: opts.forceLink,
538
- });
539
535
  emit({
540
- data: { setup: "init", init: data, buildLink: bl },
536
+ data: { setup: "init", ...data },
541
537
  json: opts.json,
542
538
  help: [],
543
539
  });
544
540
  break;
545
541
  }
542
+
543
+ if (!remote) {
544
+ fail(
545
+ "setup needs a remote (or --init for local skeleton).\n" +
546
+ " ai-md setup --remote https://github.com/<you>/.ai-md.git\n" +
547
+ " ai-md init\n" +
548
+ " (auto-detect: authenticate `gh` and create github.com/<user>/.ai-md)",
549
+ { exitCode: 2, json: opts.json }
550
+ );
551
+ process.exit(2);
552
+ }
553
+
554
+ // Remote known → clone/sync first; never seed skeleton before sync
546
555
  const saved = writeMachineConfig(
547
556
  {
548
- dir: opts.dir || cfg.dir,
549
- remote: opts.remote || cfg.remote,
550
- agents: opts.agents || cfg.agents,
551
- linkMode: opts.linkMode || cfg.linkMode,
557
+ dir: opts.dir || remoteCfg.dir,
558
+ remote,
559
+ agents: opts.agents || remoteCfg.agents,
560
+ linkMode: opts.linkMode || remoteCfg.linkMode,
552
561
  },
553
562
  process.env,
554
563
  { dryRun: opts.dryRun }
555
564
  );
556
565
  cfg = resolveConfig(process.env, {
557
566
  dir: opts.dir || undefined,
558
- remote: opts.remote || undefined,
567
+ remote,
568
+ skipRemoteDetect: true,
559
569
  });
560
570
  applyEnvFromConfig(cfg);
561
- const result = opts.dryRun
562
- ? { build: null, link: null }
563
- : runInstall(cfg, {
564
- agents: agentsOrDefault(opts, cfg),
565
- force: opts.force,
566
- forceLink: opts.forceLink,
567
- });
571
+ const result = bootstrapContent(cfg, {
572
+ force: opts.force,
573
+ dryRun: opts.dryRun,
574
+ forceLink: opts.forceLink,
575
+ agents: agentsOrDefault(opts, cfg),
576
+ });
568
577
  const scripts =
569
578
  opts.scripts.length === 0
570
579
  ? []
@@ -578,7 +587,7 @@ function main() {
578
587
  from: opts.from,
579
588
  });
580
589
  emit({
581
- data: { setup: "ok", config: saved, ...result, scripts, ...data },
590
+ data: { setup: "ok", config: saved, result, scripts, ...data },
582
591
  json: opts.json,
583
592
  help: statusHelp(data),
584
593
  });
package/lib/commands.js CHANGED
@@ -22,7 +22,7 @@ const {
22
22
  ensureAgentSourceDirs,
23
23
  isHarnessInstalled,
24
24
  } = require("./harnesses");
25
- const { seedSkeleton, initRepo, SKELETON_VERSION } = require("./skeleton");
25
+ const { seedSkeleton, initRepo, SKELETON_VERSION, isEmptyDir } = require("./skeleton");
26
26
 
27
27
  function ensureGitignore(repoPath, { dryRun = false } = {}) {
28
28
  const gitignore = path.join(repoPath, ".gitignore");
@@ -259,16 +259,39 @@ function buildAndLink(cfg, opts = {}) {
259
259
 
260
260
  function runInstall(cfg, opts = {}) {
261
261
  const dir = cfg.dir;
262
- if (!fs.existsSync(dir)) {
262
+ const empty = !fs.existsSync(dir) || isEmptyDir(dir);
263
+
264
+ if (empty) {
265
+ if (!cfg.remote) {
266
+ const err = new Error(
267
+ "No remote configured and content dir is empty.\n" +
268
+ " ai-md init # skeleton only (no remote)\n" +
269
+ " ai-md setup --remote <url> # clone existing github.com/<user>/.ai-md"
270
+ );
271
+ err.code = "EINVAL";
272
+ throw err;
273
+ }
274
+ if (fs.existsSync(dir) && isEmptyDir(dir)) {
275
+ fs.rmSync(dir, { recursive: true, force: true });
276
+ }
263
277
  gitClone(cfg.remote, dir, { dryRun: opts.dryRun });
278
+ if (!opts.dryRun) {
279
+ seedSkeleton(cfg, { force: false });
280
+ }
264
281
  } else if (!fs.existsSync(path.join(dir, ".git"))) {
265
282
  const err = new Error(
266
- `${dir} exists but is not a git repo. Use ai-md init or remove it.`
283
+ `${dir} exists but is not a git repo. Move it aside, then: ai-md setup --remote <url>`
267
284
  );
268
285
  err.code = "EEXIST";
269
286
  throw err;
287
+ } else if (cfg.remote && opts.pull !== false) {
288
+ try {
289
+ gitPull(cfg.dir, { dryRun: opts.dryRun });
290
+ } catch {
291
+ /* offline / no upstream — continue with local tree */
292
+ }
270
293
  }
271
- // migrate hint: if legacy flat rules/ exist, status will note
294
+
272
295
  return buildAndLink(cfg, {
273
296
  agents: opts.agents,
274
297
  force: opts.force,
@@ -277,6 +300,111 @@ function runInstall(cfg, opts = {}) {
277
300
  });
278
301
  }
279
302
 
303
+ /**
304
+ * Bootstrap: remote set → clone/sync first (never skeleton-first).
305
+ * No remote → seed skeleton only.
306
+ */
307
+ function bootstrapContent(cfg, opts = {}) {
308
+ const {
309
+ noGit = false,
310
+ force = false,
311
+ dryRun = false,
312
+ forceLink = false,
313
+ agents = ["cursor"],
314
+ } = opts;
315
+
316
+ if (cfg.remote) {
317
+ const dir = cfg.dir;
318
+ const exists = fs.existsSync(dir);
319
+ const empty = !exists || isEmptyDir(dir);
320
+ const isGit = exists && fs.existsSync(path.join(dir, ".git"));
321
+
322
+ if (!empty && isGit) {
323
+ if (!dryRun) {
324
+ writeMachineConfig({
325
+ dir: cfg.dir,
326
+ remote: cfg.remote,
327
+ agents,
328
+ linkMode: cfg.linkMode,
329
+ });
330
+ }
331
+ return {
332
+ action: "synced",
333
+ remote: cfg.remote,
334
+ dir,
335
+ ...runInstall(cfg, {
336
+ agents,
337
+ force,
338
+ dryRun,
339
+ forceLink,
340
+ pull: true,
341
+ }),
342
+ };
343
+ }
344
+
345
+ if (!empty && !isGit) {
346
+ if (!force) {
347
+ const err = new Error(
348
+ `${dir} is not empty and has no .git.\n` +
349
+ ` mv ${dir} ${dir}.bak && ai-md setup --remote ${cfg.remote}\n` +
350
+ ` (refusing to seed skeleton before sync when a remote is set)`
351
+ );
352
+ err.code = "EEXIST";
353
+ throw err;
354
+ }
355
+ if (dryRun) {
356
+ return { action: "would_replace_and_clone", remote: cfg.remote, dir };
357
+ }
358
+ fs.rmSync(dir, { recursive: true, force: true });
359
+ }
360
+
361
+ if (dryRun) {
362
+ return { action: "would_clone", remote: cfg.remote, dir };
363
+ }
364
+ if (fs.existsSync(dir) && isEmptyDir(dir)) {
365
+ fs.rmSync(dir, { recursive: true, force: true });
366
+ }
367
+ gitClone(cfg.remote, dir, { dryRun: false });
368
+ // After clone only — fill gaps; never invent a tree instead of syncing
369
+ seedSkeleton(cfg, { force: false });
370
+ writeMachineConfig({
371
+ dir: cfg.dir,
372
+ remote: cfg.remote,
373
+ agents,
374
+ linkMode: cfg.linkMode,
375
+ });
376
+ const fresh = resolveConfig(process.env, {
377
+ dir: cfg.dir,
378
+ remote: cfg.remote,
379
+ skipRemoteDetect: true,
380
+ });
381
+ return {
382
+ action: "cloned",
383
+ remote: cfg.remote,
384
+ dir,
385
+ ...buildAndLink(fresh, { agents, force: true, forceLink }),
386
+ };
387
+ }
388
+
389
+ const data = initRepo(cfg, { noGit, force, dryRun });
390
+ if (!dryRun) {
391
+ writeMachineConfig({
392
+ dir: cfg.dir,
393
+ agents,
394
+ linkMode: cfg.linkMode,
395
+ });
396
+ }
397
+ const fresh = resolveConfig(process.env, {
398
+ dir: cfg.dir,
399
+ skipRemoteDetect: true,
400
+ });
401
+ let bl = null;
402
+ if (!dryRun) {
403
+ bl = buildAndLink(fresh, { agents, force: true, forceLink });
404
+ }
405
+ return { action: "initialized", init: data, buildLink: bl };
406
+ }
407
+
280
408
  function runPull(cfg, opts = {}) {
281
409
  if (!fs.existsSync(path.join(cfg.dir, ".git"))) {
282
410
  const err = new Error(`${cfg.dir} is not a git repo; run ai-md install`);
@@ -443,6 +571,7 @@ module.exports = {
443
571
  runRescue,
444
572
  seedSkeleton,
445
573
  initRepo,
574
+ bootstrapContent,
446
575
  harnessList,
447
576
  harnessShow,
448
577
  harnessSet,
package/lib/config.js CHANGED
@@ -4,8 +4,10 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { execFileSync } = require("child_process");
6
6
  const { expandHome } = require("./config-paths");
7
+ const { detectDefaultRemote } = require("./remote");
7
8
 
8
- const DEFAULT_REMOTE = "https://github.com/dujavi/.ai-md.git";
9
+ /** @deprecated No hardcoded personal default — use detectDefaultRemote(). */
10
+ const DEFAULT_REMOTE = null;
9
11
 
10
12
  function machineConfigPath(env = process.env) {
11
13
  if (env.AI_MD_CONFIG) return expandHome(env.AI_MD_CONFIG);
@@ -67,7 +69,24 @@ function resolveConfig(env = process.env, opts = {}) {
67
69
  const envRemote = env.AI_MD_REMOTE || env.CURSOR_MD_REMOTE || null;
68
70
 
69
71
  const dir = expandHome(opts.dir || envDir || stored.dir || defaultDir);
70
- const remote = opts.remote || envRemote || stored.remote || DEFAULT_REMOTE;
72
+
73
+ let remote = opts.remote || envRemote || stored.remote || null;
74
+ let remoteSource = opts.remote
75
+ ? "flag"
76
+ : envRemote
77
+ ? "env"
78
+ : stored.remote
79
+ ? "config"
80
+ : "none";
81
+ let remoteDetection = null;
82
+
83
+ if (!remote && opts.skipRemoteDetect !== true) {
84
+ remoteDetection = detectDefaultRemote();
85
+ if (remoteDetection.remote) {
86
+ remote = remoteDetection.remote;
87
+ remoteSource = "detected";
88
+ }
89
+ }
71
90
 
72
91
  const sources = {
73
92
  dir: opts.dir
@@ -77,13 +96,7 @@ function resolveConfig(env = process.env, opts = {}) {
77
96
  : stored.dir
78
97
  ? "config"
79
98
  : "default",
80
- remote: opts.remote
81
- ? "flag"
82
- : envRemote && !(stored.remote && envRemote === stored.remote)
83
- ? "env"
84
- : stored.remote
85
- ? "config"
86
- : "default",
99
+ remote: remoteSource,
87
100
  };
88
101
 
89
102
  const raw = stored.raw || {};
@@ -98,6 +111,7 @@ function resolveConfig(env = process.env, opts = {}) {
98
111
  dir,
99
112
  remote,
100
113
  sources,
114
+ remoteDetection,
101
115
  agents,
102
116
  linkMode,
103
117
  machineConfigPath: stored.path,
@@ -118,9 +132,17 @@ function resolveConfig(env = process.env, opts = {}) {
118
132
 
119
133
  function applyEnvFromConfig(cfg) {
120
134
  process.env.AI_MD_DIR = cfg.dir;
121
- process.env.AI_MD_REMOTE = cfg.remote;
122
135
  process.env.CURSOR_MD_DIR = cfg.dir;
123
- process.env.CURSOR_MD_REMOTE = cfg.remote;
136
+ // Never invent remotes into the environment (avoids fake defaults and
137
+ // flipping sources.remote to "env" on re-resolve). Only pass through when
138
+ // the process already had one, or the caller passed --remote (flag).
139
+ if (
140
+ cfg.remote &&
141
+ (cfg.sources.remote === "env" || cfg.sources.remote === "flag")
142
+ ) {
143
+ process.env.AI_MD_REMOTE = cfg.remote;
144
+ process.env.CURSOR_MD_REMOTE = cfg.remote;
145
+ }
124
146
  }
125
147
 
126
148
  function templatePath(cfg, name = "base") {
@@ -201,6 +223,13 @@ function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
201
223
  }
202
224
 
203
225
  function gitClone(remote, dir, { dryRun = false } = {}) {
226
+ if (!remote) {
227
+ const err = new Error(
228
+ "git clone requires a remote URL (pass --remote or set AI_MD_REMOTE)"
229
+ );
230
+ err.code = "EINVAL";
231
+ throw err;
232
+ }
204
233
  if (dryRun) return { action: "would_clone", remote, dir };
205
234
  fs.mkdirSync(path.dirname(dir), { recursive: true });
206
235
  execFileSync("git", ["clone", remote, dir], { stdio: "inherit" });
package/lib/remote.js ADDED
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+
3
+ const { execFileSync } = require("child_process");
4
+
5
+ function runCapture(cmd, args, opts = {}) {
6
+ try {
7
+ return execFileSync(cmd, args, {
8
+ encoding: "utf8",
9
+ stdio: ["ignore", "pipe", "pipe"],
10
+ timeout: opts.timeout || 20000,
11
+ }).trim();
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * GitHub login from authenticated `gh`, else git config github.user.
19
+ */
20
+ function detectGithubUser() {
21
+ const fromGh = runCapture("gh", ["api", "user", "-q", ".login"]);
22
+ if (fromGh && !fromGh.includes(" ") && /^[A-Za-z0-9-]+$/.test(fromGh)) {
23
+ return { user: fromGh, via: "gh" };
24
+ }
25
+ const fromGit = runCapture("git", ["config", "--global", "github.user"]);
26
+ if (fromGit && /^[A-Za-z0-9-]+$/.test(fromGit)) {
27
+ return { user: fromGit, via: "git" };
28
+ }
29
+ return { user: null, via: null };
30
+ }
31
+
32
+ function candidateRemotes(user) {
33
+ return [
34
+ `https://github.com/${user}/.ai-md.git`,
35
+ `git@github.com:${user}/.ai-md.git`,
36
+ ];
37
+ }
38
+
39
+ function remoteReachable(url) {
40
+ const out = runCapture("git", ["ls-remote", "--exit-code", url, "HEAD"], {
41
+ timeout: 25000,
42
+ });
43
+ // ls-remote --exit-code returns null from runCapture on failure
44
+ // success returns stdout (may be empty-ish but non-null string with sha)
45
+ return out != null;
46
+ }
47
+
48
+ function repoExistsViaGh(user) {
49
+ const url = runCapture("gh", [
50
+ "repo",
51
+ "view",
52
+ `${user}/.ai-md`,
53
+ "--json",
54
+ "url",
55
+ "-q",
56
+ ".url",
57
+ ]);
58
+ return Boolean(url);
59
+ }
60
+
61
+ /**
62
+ * Only invent a default remote when gh/git identifies a user AND
63
+ * github.com/{user}/.ai-md exists.
64
+ */
65
+ function detectDefaultRemote() {
66
+ const { user, via } = detectGithubUser();
67
+ if (!user) {
68
+ return {
69
+ remote: null,
70
+ source: "none",
71
+ githubUser: null,
72
+ githubVia: null,
73
+ verified: false,
74
+ };
75
+ }
76
+
77
+ if (repoExistsViaGh(user)) {
78
+ return {
79
+ remote: `https://github.com/${user}/.ai-md.git`,
80
+ source: "detected",
81
+ githubUser: user,
82
+ githubVia: via,
83
+ verified: true,
84
+ };
85
+ }
86
+
87
+ for (const url of candidateRemotes(user)) {
88
+ if (remoteReachable(url)) {
89
+ return {
90
+ remote: url.startsWith("git@")
91
+ ? `https://github.com/${user}/.ai-md.git`
92
+ : url,
93
+ source: "detected",
94
+ githubUser: user,
95
+ githubVia: via,
96
+ verified: true,
97
+ };
98
+ }
99
+ }
100
+
101
+ return {
102
+ remote: null,
103
+ source: "none",
104
+ githubUser: user,
105
+ githubVia: via,
106
+ verified: false,
107
+ hint: `No github.com/${user}/.ai-md repo found (create it or pass --remote)`,
108
+ };
109
+ }
110
+
111
+ module.exports = {
112
+ detectGithubUser,
113
+ detectDefaultRemote,
114
+ remoteReachable,
115
+ candidateRemotes,
116
+ };
package/lib/status.js CHANGED
@@ -184,8 +184,11 @@ function collectStatus(opts = {}) {
184
184
  return {
185
185
  generatedAt: new Date().toISOString(),
186
186
  dir: cfg.dir,
187
- remote: remote || cfg.remote,
187
+ remote: remote || cfg.remote || null,
188
+ configuredRemote: cfg.remote || null,
189
+ gitRemote: remote,
188
190
  sources: cfg.sources,
191
+ remoteDetection: cfg.remoteDetection,
189
192
  linkMode: cfg.linkMode,
190
193
  wsl: isWsl(),
191
194
  machineConfigPath: cfg.machineConfigPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dujavi/ai-md",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "AXI-shaped CLI for private ~/.ai-md: shared+agents build to dist, multi-harness link (Cursor, Claude, agents, …)",
5
5
  "bin": {
6
6
  "ai-md": "bin/ai-md.js"
@@ -4,7 +4,7 @@
4
4
  set -euo pipefail
5
5
 
6
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}}"
7
+ REMOTE_URL="${AI_MD_REMOTE:-${CURSOR_MD_REMOTE:-}}"
8
8
  DRY_RUN=0
9
9
  FORCE=0
10
10
  COMMIT_MSG="Update personal AI skills/rules"
@@ -32,10 +32,10 @@ Options:
32
32
 
33
33
  Environment:
34
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)
35
+ AI_MD_REMOTE Clone URL if repo missing (required for install clone; no hardcoded default)
36
36
 
37
37
  Examples:
38
- ai-md install
38
+ AI_MD_REMOTE=https://github.com/<you>/.ai-md.git ai-md install
39
39
  ai-md pull
40
40
  ai-md push -m "Add grafana rule"
41
41
  ai-md status
@@ -82,6 +82,13 @@ clone_if_missing() {
82
82
  fi
83
83
  if [[ -d "$REPO" ]] && [[ -n "$(ls -A "$REPO" 2>/dev/null || true)" ]]; then
84
84
  err "$REPO exists but is not a git repo (and is not empty)"
85
+ err " Move it aside, then clone — do not seed a skeleton over an unsynced remote tree"
86
+ exit 1
87
+ fi
88
+ if [[ -z "$REMOTE_URL" ]]; then
89
+ err "No remote set and $REPO is missing/empty."
90
+ err " export AI_MD_REMOTE=https://github.com/<you>/.ai-md.git"
91
+ err " or: ai-md setup --remote <url> / ai-md init"
85
92
  exit 1
86
93
  fi
87
94
  log "Cloning $REMOTE_URL → $REPO"