caveat-cli 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -9761,49 +9761,49 @@ var require_fast_uri = __commonJS({
9761
9761
  schemelessOptions.skipEscape = true;
9762
9762
  return serialize(resolved, schemelessOptions);
9763
9763
  }
9764
- function resolveComponent(base2, relative2, options2, skipNormalization) {
9764
+ function resolveComponent(base2, relative3, options2, skipNormalization) {
9765
9765
  const target = {};
9766
9766
  if (!skipNormalization) {
9767
9767
  base2 = parse4(serialize(base2, options2), options2);
9768
- relative2 = parse4(serialize(relative2, options2), options2);
9768
+ relative3 = parse4(serialize(relative3, options2), options2);
9769
9769
  }
9770
9770
  options2 = options2 || {};
9771
- if (!options2.tolerant && relative2.scheme) {
9772
- target.scheme = relative2.scheme;
9773
- target.userinfo = relative2.userinfo;
9774
- target.host = relative2.host;
9775
- target.port = relative2.port;
9776
- target.path = removeDotSegments(relative2.path || "");
9777
- target.query = relative2.query;
9771
+ if (!options2.tolerant && relative3.scheme) {
9772
+ target.scheme = relative3.scheme;
9773
+ target.userinfo = relative3.userinfo;
9774
+ target.host = relative3.host;
9775
+ target.port = relative3.port;
9776
+ target.path = removeDotSegments(relative3.path || "");
9777
+ target.query = relative3.query;
9778
9778
  } else {
9779
- if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
9780
- target.userinfo = relative2.userinfo;
9781
- target.host = relative2.host;
9782
- target.port = relative2.port;
9783
- target.path = removeDotSegments(relative2.path || "");
9784
- target.query = relative2.query;
9779
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
9780
+ target.userinfo = relative3.userinfo;
9781
+ target.host = relative3.host;
9782
+ target.port = relative3.port;
9783
+ target.path = removeDotSegments(relative3.path || "");
9784
+ target.query = relative3.query;
9785
9785
  } else {
9786
- if (!relative2.path) {
9786
+ if (!relative3.path) {
9787
9787
  target.path = base2.path;
9788
- if (relative2.query !== void 0) {
9789
- target.query = relative2.query;
9788
+ if (relative3.query !== void 0) {
9789
+ target.query = relative3.query;
9790
9790
  } else {
9791
9791
  target.query = base2.query;
9792
9792
  }
9793
9793
  } else {
9794
- if (relative2.path[0] === "/") {
9795
- target.path = removeDotSegments(relative2.path);
9794
+ if (relative3.path[0] === "/") {
9795
+ target.path = removeDotSegments(relative3.path);
9796
9796
  } else {
9797
9797
  if ((base2.userinfo !== void 0 || base2.host !== void 0 || base2.port !== void 0) && !base2.path) {
9798
- target.path = "/" + relative2.path;
9798
+ target.path = "/" + relative3.path;
9799
9799
  } else if (!base2.path) {
9800
- target.path = relative2.path;
9800
+ target.path = relative3.path;
9801
9801
  } else {
9802
- target.path = base2.path.slice(0, base2.path.lastIndexOf("/") + 1) + relative2.path;
9802
+ target.path = base2.path.slice(0, base2.path.lastIndexOf("/") + 1) + relative3.path;
9803
9803
  }
9804
9804
  target.path = removeDotSegments(target.path);
9805
9805
  }
9806
- target.query = relative2.query;
9806
+ target.query = relative3.query;
9807
9807
  }
9808
9808
  target.userinfo = base2.userinfo;
9809
9809
  target.host = base2.host;
@@ -9811,7 +9811,7 @@ var require_fast_uri = __commonJS({
9811
9811
  }
9812
9812
  target.scheme = base2.scheme;
9813
9813
  }
9814
- target.fragment = relative2.fragment;
9814
+ target.fragment = relative3.fragment;
9815
9815
  return target;
9816
9816
  }
9817
9817
  function equal(uriA, uriB, options2) {
@@ -16139,10 +16139,12 @@ function resolvePaths(caveatHome, knowledgeRepo, userHome) {
16139
16139
 
16140
16140
  // ../../packages/core/dist/config.js
16141
16141
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
16142
+ var SHARED_REPO_URL = "https://github.com/kitepon-rgb/caveats-quo";
16142
16143
  var DEFAULT_CONFIG = {
16143
16144
  knowledgeRepo: "own",
16144
16145
  semverKeys: ["driver", "cuda", "node"],
16145
- communitySources: []
16146
+ communitySources: [],
16147
+ sharedRepo: SHARED_REPO_URL
16146
16148
  };
16147
16149
  function loadConfig(userConfigPath) {
16148
16150
  const userCfg = existsSync5(userConfigPath) ? JSON.parse(readFileSync4(userConfigPath, "utf-8")) : {};
@@ -21251,7 +21253,7 @@ var stdoutLogger = {
21251
21253
  };
21252
21254
 
21253
21255
  // src/commands/init.ts
21254
- import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "node:fs";
21256
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
21255
21257
  import { join as join9 } from "node:path";
21256
21258
 
21257
21259
  // src/claudeInstall.ts
@@ -21403,7 +21405,21 @@ function uninstallClaudeIntegration(opts) {
21403
21405
  }
21404
21406
 
21405
21407
  // src/commands/init.ts
21406
- function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21408
+ var KNOWLEDGE_GITIGNORE = [
21409
+ "# Never commit entries flagged private (visibility: private is enforced by the",
21410
+ "# pre-commit gate, but this is a filename-level backup guard).",
21411
+ "*.private.md",
21412
+ "",
21413
+ "# Obsidian per-user config: workspace layout, theme, plugin state, cache.",
21414
+ ".obsidian/",
21415
+ "",
21416
+ "# community/ is a local cache of shallow-cloned third-party caveat repos",
21417
+ "# (populated by `caveat community add`). Each contains its own .git and is not",
21418
+ "# part of this repo's tracked knowledge.",
21419
+ "community/",
21420
+ ""
21421
+ ].join("\n");
21422
+ async function runInit(ctx, opts = { skipClaude: false, skipShared: false, dryRun: false }) {
21407
21423
  ensureUserConfig(ctx.userConfigPath);
21408
21424
  ctx.logger.info(`user config: ${ctx.userConfigPath}`);
21409
21425
  if (!existsSync8(ctx.paths.knowledgeRepo)) {
@@ -21413,8 +21429,23 @@ function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21413
21429
  } else {
21414
21430
  ctx.logger.info(`knowledge repo: ${ctx.paths.knowledgeRepo}`);
21415
21431
  }
21432
+ const gitignorePath = join9(ctx.paths.knowledgeRepo, ".gitignore");
21433
+ if (!existsSync8(gitignorePath)) {
21434
+ writeFileSync5(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
21435
+ ctx.logger.info(`.gitignore created: ${gitignorePath}`);
21436
+ }
21416
21437
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
21417
- db.close();
21438
+ try {
21439
+ if (!opts.skipShared && !opts.dryRun) {
21440
+ await subscribeSharedRepo(ctx, db);
21441
+ } else if (opts.skipShared) {
21442
+ ctx.logger.info("shared community DB subscription skipped (--skip-shared)");
21443
+ } else if (opts.dryRun) {
21444
+ ctx.logger.info(`[dry-run] would subscribe to shared community DB: ${ctx.config.sharedRepo}`);
21445
+ }
21446
+ } finally {
21447
+ db.close();
21448
+ }
21418
21449
  ctx.logger.info(`db initialized: ${ctx.paths.dbPath}`);
21419
21450
  if (opts.skipClaude) {
21420
21451
  ctx.logger.info("Claude Code integration skipped (--skip-claude)");
@@ -21434,6 +21465,52 @@ function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21434
21465
  });
21435
21466
  reportInstallResult(ctx, result, opts.dryRun);
21436
21467
  }
21468
+ async function subscribeSharedRepo(ctx, db) {
21469
+ const url = ctx.config.sharedRepo;
21470
+ const validation = validateCommunityUrl(url);
21471
+ if (!validation.valid) {
21472
+ ctx.logger.warn(
21473
+ `sharedRepo is not a valid GitHub URL \u2014 skipping: ${validation.reason}`
21474
+ );
21475
+ return;
21476
+ }
21477
+ const handle = validation.handle;
21478
+ const target = join9(ctx.paths.communityDir, handle);
21479
+ if (!existsSync8(target)) {
21480
+ if (!existsSync8(ctx.paths.communityDir)) {
21481
+ mkdirSync4(ctx.paths.communityDir, { recursive: true });
21482
+ }
21483
+ try {
21484
+ await communityAdd({
21485
+ url,
21486
+ communityDir: ctx.paths.communityDir,
21487
+ logger: ctx.logger
21488
+ });
21489
+ ctx.logger.info(`shared community DB subscribed: ${url} \u2192 community/${handle}/`);
21490
+ } catch (err) {
21491
+ ctx.logger.warn(
21492
+ `shared community DB subscription failed: ${err instanceof Error ? err.message : String(err)}`
21493
+ );
21494
+ return;
21495
+ }
21496
+ } else {
21497
+ ctx.logger.info(`shared community DB already subscribed: community/${handle}/`);
21498
+ }
21499
+ if (existsSync8(ctx.paths.communityDir)) {
21500
+ rebuildAll(db);
21501
+ if (existsSync8(ctx.paths.entriesDir)) {
21502
+ scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
21503
+ }
21504
+ for (const entry of readdirSync4(ctx.paths.communityDir, { withFileTypes: true })) {
21505
+ if (!entry.isDirectory()) continue;
21506
+ const source = `community/${entry.name}`;
21507
+ const root = join9(ctx.paths.communityDir, entry.name, "entries");
21508
+ if (!existsSync8(root)) continue;
21509
+ const result = scanSource({ db, source, entriesRoot: root });
21510
+ ctx.logger.info(`indexed ${source}: +${result.added}`);
21511
+ }
21512
+ }
21513
+ }
21437
21514
  function runUninstall(ctx, opts) {
21438
21515
  const cliScriptPath = process.argv[1];
21439
21516
  if (!cliScriptPath) {
@@ -21478,7 +21555,7 @@ function reportInstallResult(ctx, result, dryRun) {
21478
21555
  }
21479
21556
 
21480
21557
  // src/commands/indexCmd.ts
21481
- import { existsSync as existsSync9, readdirSync as readdirSync4, mkdirSync as mkdirSync5 } from "node:fs";
21558
+ import { existsSync as existsSync9, readdirSync as readdirSync5, mkdirSync as mkdirSync5 } from "node:fs";
21482
21559
  import { dirname as dirname5, join as join10 } from "node:path";
21483
21560
  function runIndex(ctx, opts) {
21484
21561
  const dbDir = dirname5(ctx.paths.dbPath);
@@ -21496,7 +21573,7 @@ function runIndex(ctx, opts) {
21496
21573
  ctx.logger.warn(`entries dir not found: ${ctx.paths.entriesDir}`);
21497
21574
  }
21498
21575
  if (existsSync9(ctx.paths.communityDir)) {
21499
- for (const entry of readdirSync4(ctx.paths.communityDir, { withFileTypes: true })) {
21576
+ for (const entry of readdirSync5(ctx.paths.communityDir, { withFileTypes: true })) {
21500
21577
  if (!entry.isDirectory()) continue;
21501
21578
  const source = `community/${entry.name}`;
21502
21579
  const root = join10(ctx.paths.communityDir, entry.name, "entries");
@@ -30005,12 +30082,12 @@ function createDetailRoute(ctx) {
30005
30082
  }
30006
30083
 
30007
30084
  // ../web/dist/routes/community.js
30008
- import { existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
30085
+ import { existsSync as existsSync10, readdirSync as readdirSync6, statSync as statSync4 } from "node:fs";
30009
30086
  import { join as join12 } from "node:path";
30010
30087
  function listCommunity(communityDir, db) {
30011
30088
  if (!existsSync10(communityDir)) return [];
30012
30089
  const handles = [];
30013
- for (const entry of readdirSync5(communityDir, { withFileTypes: true })) {
30090
+ for (const entry of readdirSync6(communityDir, { withFileTypes: true })) {
30014
30091
  if (!entry.isDirectory()) continue;
30015
30092
  const handlePath = join12(communityDir, entry.name);
30016
30093
  const countRow = db.prepare("SELECT COUNT(*) AS n FROM entries WHERE source = ?").get(`community/${entry.name}`);
@@ -44629,6 +44706,263 @@ async function runHook(name) {
44629
44706
  process.exit(0);
44630
44707
  }
44631
44708
 
44709
+ // src/commands/pull.ts
44710
+ import { existsSync as existsSync11, readdirSync as readdirSync7 } from "node:fs";
44711
+ import { join as join14 } from "node:path";
44712
+ async function runPull(ctx) {
44713
+ if (!existsSync11(ctx.paths.communityDir)) {
44714
+ ctx.logger.info(
44715
+ "no community repos yet \u2014 did you run `caveat init`? You can also `caveat community add <url>` manually."
44716
+ );
44717
+ return;
44718
+ }
44719
+ const results = await communityPull({
44720
+ communityDir: ctx.paths.communityDir,
44721
+ logger: ctx.logger
44722
+ });
44723
+ for (const r2 of results) {
44724
+ if (r2.status === "ok") {
44725
+ ctx.logger.info(`${r2.handle}: pulled`);
44726
+ } else {
44727
+ ctx.logger.warn(`${r2.handle}: FAILED \u2014 ${r2.message ?? "unknown"}`);
44728
+ }
44729
+ }
44730
+ const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
44731
+ try {
44732
+ rebuildAll(db);
44733
+ if (existsSync11(ctx.paths.entriesDir)) {
44734
+ const ownResult = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
44735
+ ctx.logger.info(`own: +${ownResult.added}`);
44736
+ }
44737
+ for (const entry of readdirSync7(ctx.paths.communityDir, { withFileTypes: true })) {
44738
+ if (!entry.isDirectory()) continue;
44739
+ const source = `community/${entry.name}`;
44740
+ const root = join14(ctx.paths.communityDir, entry.name, "entries");
44741
+ if (!existsSync11(root)) continue;
44742
+ const scan = scanSource({ db, source, entriesRoot: root });
44743
+ ctx.logger.info(`${source}: +${scan.added}`);
44744
+ }
44745
+ } finally {
44746
+ db.close();
44747
+ }
44748
+ }
44749
+
44750
+ // src/commands/push.ts
44751
+ import { spawnSync as spawnSync2 } from "node:child_process";
44752
+ import {
44753
+ copyFileSync as copyFileSync2,
44754
+ existsSync as existsSync12,
44755
+ mkdirSync as mkdirSync6,
44756
+ readdirSync as readdirSync8,
44757
+ readFileSync as readFileSync6,
44758
+ statSync as statSync5
44759
+ } from "node:fs";
44760
+ import { dirname as dirname6, join as join15, relative as relative2 } from "node:path";
44761
+ async function runPush(ctx, opts) {
44762
+ const ghOk = checkGhAvailable(ctx);
44763
+ if (!ghOk) return;
44764
+ const sharedValidation = validateCommunityUrl(ctx.config.sharedRepo);
44765
+ if (!sharedValidation.valid) {
44766
+ ctx.logger.error(
44767
+ `sharedRepo is not a valid GitHub URL: ${ctx.config.sharedRepo} (${sharedValidation.reason})`
44768
+ );
44769
+ process.exitCode = 1;
44770
+ return;
44771
+ }
44772
+ const [sharedOwner, sharedName] = parseOwnerRepo(ctx.config.sharedRepo);
44773
+ if (!sharedOwner || !sharedName) {
44774
+ ctx.logger.error(`cannot parse sharedRepo owner/repo: ${ctx.config.sharedRepo}`);
44775
+ process.exitCode = 1;
44776
+ return;
44777
+ }
44778
+ const owned = findOwnedEntry(ctx, opts.id);
44779
+ if (!owned) {
44780
+ ctx.logger.error(
44781
+ `entry not found in ${ctx.paths.entriesDir}: id=${opts.id}. List with \`caveat list\`, or write the md file and index first.`
44782
+ );
44783
+ process.exitCode = 1;
44784
+ return;
44785
+ }
44786
+ const ghUser = resolveGhUser(ctx);
44787
+ if (!ghUser) return;
44788
+ if (opts.dryRun) {
44789
+ ctx.logger.info(`[dry-run] would push entry ${owned.id} (${owned.title})`);
44790
+ ctx.logger.info(`[dry-run] 1. fork ${sharedOwner}/${sharedName} as ${ghUser}/${sharedName}`);
44791
+ ctx.logger.info(`[dry-run] 2. clone fork \u2192 ${forkStagingDir(ctx)}`);
44792
+ ctx.logger.info(`[dry-run] 3. copy entry \u2192 entries/${owned.relPath}`);
44793
+ ctx.logger.info("[dry-run] 4. commit, push branch, open PR against main");
44794
+ return;
44795
+ }
44796
+ ensureFork(ctx, sharedOwner, sharedName, ghUser);
44797
+ const stagingDir = forkStagingDir(ctx);
44798
+ ensureStagingClone(ctx, stagingDir, ghUser, sharedName, sharedOwner);
44799
+ const branch = `caveat-push-${owned.id}-${Date.now().toString(36)}`;
44800
+ run(ctx, "git", ["checkout", "-b", branch], { cwd: stagingDir });
44801
+ const destPath = join15(stagingDir, "entries", owned.relPath);
44802
+ mkdirSync6(dirname6(destPath), { recursive: true });
44803
+ copyFileSync2(owned.absPath, destPath);
44804
+ run(ctx, "git", ["add", join15("entries", owned.relPath)], { cwd: stagingDir });
44805
+ const verb = existsInUpstream(stagingDir, "entries/" + owned.relPath) ? "update" : "add";
44806
+ const commitMsg = `${verb}: ${owned.title}`;
44807
+ run(ctx, "git", ["commit", "-m", commitMsg], { cwd: stagingDir });
44808
+ run(ctx, "git", ["push", "--set-upstream", "origin", branch], { cwd: stagingDir });
44809
+ const prTitle = `${verb}: ${owned.title}`;
44810
+ const prBody = [
44811
+ `Contribution of caveat \`${owned.id}\` via \`caveat push\`.`,
44812
+ "",
44813
+ `- entry path: \`entries/${owned.relPath}\``,
44814
+ `- action: ${verb}`,
44815
+ "",
44816
+ "Merged PRs will appear in subscribers' repos on their next `caveat pull`."
44817
+ ].join("\n");
44818
+ const prResult = runClaim(
44819
+ ctx,
44820
+ "gh",
44821
+ [
44822
+ "pr",
44823
+ "create",
44824
+ "--repo",
44825
+ `${sharedOwner}/${sharedName}`,
44826
+ "--base",
44827
+ "main",
44828
+ "--head",
44829
+ `${ghUser}:${branch}`,
44830
+ "--title",
44831
+ prTitle,
44832
+ "--body",
44833
+ prBody
44834
+ ],
44835
+ { cwd: stagingDir }
44836
+ );
44837
+ if (prResult.status === 0) {
44838
+ const url = prResult.stdout.trim();
44839
+ ctx.logger.info(`PR opened: ${url}`);
44840
+ ctx.logger.info(
44841
+ "Once merged, other subscribers will see your entry after their next `caveat pull`."
44842
+ );
44843
+ } else {
44844
+ ctx.logger.error(
44845
+ `gh pr create failed: ${(prResult.stderr || prResult.stdout || "").trim()}`
44846
+ );
44847
+ process.exitCode = 1;
44848
+ }
44849
+ }
44850
+ function checkGhAvailable(ctx) {
44851
+ const r2 = spawnSync2("gh", ["--version"], { encoding: "utf-8", shell: true });
44852
+ if (r2.status !== 0) {
44853
+ ctx.logger.error(
44854
+ "gh (GitHub CLI) is required for `caveat push`. Install: https://cli.github.com/"
44855
+ );
44856
+ process.exitCode = 1;
44857
+ return false;
44858
+ }
44859
+ const auth = spawnSync2("gh", ["auth", "status"], { encoding: "utf-8", shell: true });
44860
+ if (auth.status !== 0) {
44861
+ ctx.logger.error("gh is not authenticated. Run `gh auth login` first.");
44862
+ process.exitCode = 1;
44863
+ return false;
44864
+ }
44865
+ return true;
44866
+ }
44867
+ function resolveGhUser(ctx) {
44868
+ const r2 = spawnSync2("gh", ["api", "user", "--jq", ".login"], {
44869
+ encoding: "utf-8",
44870
+ shell: true
44871
+ });
44872
+ if (r2.status !== 0) {
44873
+ ctx.logger.error(`gh api user failed: ${(r2.stderr || r2.stdout).trim()}`);
44874
+ process.exitCode = 1;
44875
+ return void 0;
44876
+ }
44877
+ return r2.stdout.trim();
44878
+ }
44879
+ function parseOwnerRepo(url) {
44880
+ const m = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?\/?$/.exec(url);
44881
+ if (!m) return [void 0, void 0];
44882
+ return [m[1], m[2]];
44883
+ }
44884
+ function findOwnedEntry(ctx, id) {
44885
+ if (!existsSync12(ctx.paths.entriesDir)) return void 0;
44886
+ for (const categoryDirent of readdirSync8(ctx.paths.entriesDir, { withFileTypes: true })) {
44887
+ if (!categoryDirent.isDirectory()) continue;
44888
+ const categoryDir = join15(ctx.paths.entriesDir, categoryDirent.name);
44889
+ for (const fileDirent of readdirSync8(categoryDir, { withFileTypes: true })) {
44890
+ if (!fileDirent.isFile() || !fileDirent.name.endsWith(".md")) continue;
44891
+ const absPath = join15(categoryDir, fileDirent.name);
44892
+ const raw2 = readFileSync6(absPath, "utf-8");
44893
+ try {
44894
+ const parsed = parseMarkdown(raw2);
44895
+ if (parsed.frontmatter.id === id) {
44896
+ const relPath = relative2(ctx.paths.entriesDir, absPath).replace(/\\/g, "/");
44897
+ return {
44898
+ id,
44899
+ title: parsed.frontmatter.title,
44900
+ absPath,
44901
+ relPath
44902
+ };
44903
+ }
44904
+ } catch {
44905
+ }
44906
+ }
44907
+ }
44908
+ return void 0;
44909
+ }
44910
+ function forkStagingDir(ctx) {
44911
+ return join15(ctx.caveatHome, "push-fork");
44912
+ }
44913
+ function ensureFork(ctx, owner, repo, ghUser) {
44914
+ const r2 = spawnSync2(
44915
+ "gh",
44916
+ ["repo", "fork", `${owner}/${repo}`, "--clone=false", "--remote=false"],
44917
+ { encoding: "utf-8", shell: true }
44918
+ );
44919
+ if (r2.status !== 0 && !(r2.stderr || "").includes("already exists")) {
44920
+ ctx.logger.warn(`gh repo fork stderr: ${r2.stderr.trim()}`);
44921
+ }
44922
+ void ghUser;
44923
+ }
44924
+ function ensureStagingClone(ctx, stagingDir, ghUser, sharedName, upstreamOwner) {
44925
+ if (!existsSync12(stagingDir)) {
44926
+ mkdirSync6(dirname6(stagingDir), { recursive: true });
44927
+ const forkUrl = `https://github.com/${ghUser}/${sharedName}.git`;
44928
+ run(ctx, "git", ["clone", "--depth", "30", forkUrl, stagingDir]);
44929
+ run(ctx, "git", ["remote", "add", "upstream", `https://github.com/${upstreamOwner}/${sharedName}.git`], {
44930
+ cwd: stagingDir
44931
+ });
44932
+ } else {
44933
+ run(ctx, "git", ["checkout", "main"], { cwd: stagingDir });
44934
+ run(ctx, "git", ["fetch", "upstream", "main"], { cwd: stagingDir });
44935
+ run(ctx, "git", ["reset", "--hard", "upstream/main"], { cwd: stagingDir });
44936
+ run(ctx, "git", ["push", "origin", "main", "--force-with-lease"], { cwd: stagingDir });
44937
+ }
44938
+ }
44939
+ function existsInUpstream(stagingDir, relFromRoot) {
44940
+ return existsSync12(join15(stagingDir, relFromRoot));
44941
+ }
44942
+ function run(ctx, command, args, opts = {}) {
44943
+ const r2 = spawnSync2(command, args, {
44944
+ encoding: "utf-8",
44945
+ cwd: opts.cwd,
44946
+ shell: true
44947
+ });
44948
+ if (r2.status !== 0) {
44949
+ const msg = (r2.stderr || r2.stdout || "unknown").trim();
44950
+ throw new Error(`${command} ${args.join(" ")} failed: ${msg}`);
44951
+ }
44952
+ }
44953
+ function runClaim(ctx, command, args, opts = {}) {
44954
+ const r2 = spawnSync2(command, args, {
44955
+ encoding: "utf-8",
44956
+ cwd: opts.cwd,
44957
+ shell: true
44958
+ });
44959
+ return {
44960
+ status: r2.status,
44961
+ stdout: typeof r2.stdout === "string" ? r2.stdout : "",
44962
+ stderr: typeof r2.stderr === "string" ? r2.stderr : ""
44963
+ };
44964
+ }
44965
+
44632
44966
  // src/commands/community.ts
44633
44967
  async function runCommunityAdd(ctx, url) {
44634
44968
  try {
@@ -44681,12 +45015,16 @@ function runCommunityList(ctx) {
44681
45015
 
44682
45016
  // src/index.ts
44683
45017
  var program = new Command();
44684
- program.name("caveat").description("External spec gotcha knowledge base CLI").version("0.2.0");
45018
+ program.name("caveat").description("External spec gotcha knowledge base CLI").version("0.3.0");
44685
45019
  program.command("init").description(
44686
- "Initialize ~/.caveatrc.json, ~/.caveat/, and register Claude Code integration"
44687
- ).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--dry-run", "show planned changes without writing", false).action((opts) => {
45020
+ "Initialize ~/.caveatrc.json, ~/.caveat/, subscribe to the shared community DB, and register Claude Code integration"
45021
+ ).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--skip-shared", "skip subscribing to the shared community DB", false).option("--dry-run", "show planned changes without writing", false).action(async (opts) => {
44688
45022
  const ctx = buildContext(stdoutLogger);
44689
- runInit(ctx, { skipClaude: opts.skipClaude, dryRun: opts.dryRun });
45023
+ await runInit(ctx, {
45024
+ skipClaude: opts.skipClaude,
45025
+ skipShared: opts.skipShared,
45026
+ dryRun: opts.dryRun
45027
+ });
44690
45028
  });
44691
45029
  program.command("uninstall").description("Remove Claude Code MCP server and hooks registered by `caveat init`").option("--dry-run", "show planned changes without writing", false).action((opts) => {
44692
45030
  const ctx = buildContext(stdoutLogger);
@@ -44719,6 +45057,16 @@ program.command("stats").description("Show aggregate stats").action(() => {
44719
45057
  const ctx = buildContext(stdoutLogger);
44720
45058
  runStats(ctx);
44721
45059
  });
45060
+ program.command("pull").description(
45061
+ "Refresh all subscribed community repos (incl. shared DB) and re-index. Use this to receive contributions others have merged."
45062
+ ).action(async () => {
45063
+ const ctx = buildContext(stdoutLogger);
45064
+ await runPull(ctx);
45065
+ });
45066
+ program.command("push").description("Contribute a caveat to the shared community DB via fork + PR (requires gh CLI).").argument("<id>", "entry id (from `caveat list`)").option("--dry-run", "show planned steps without making GitHub changes", false).action(async (id, opts) => {
45067
+ const ctx = buildContext(stdoutLogger);
45068
+ await runPush(ctx, { id, dryRun: opts.dryRun });
45069
+ });
44722
45070
  program.command("serve").description("Start the read-only web share portal").option("--port <n>", "port number", (v) => Number(v), 4242).action((opts) => {
44723
45071
  runServe({ port: opts.port });
44724
45072
  });