@mp-lb/forum-cli 0.1.2 → 0.1.6

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
@@ -1753,18 +1753,365 @@ var require_jsx_runtime = __commonJS({
1753
1753
  }
1754
1754
  });
1755
1755
 
1756
+ // src/createCheckoutIgnoreMatcher.ts
1757
+ import ignore from "ignore";
1758
+
1759
+ // src/mapSyncRulePath.ts
1760
+ var assertSafeRelativePath = (relativePath) => {
1761
+ const segments2 = relativePath.split("/");
1762
+ if (relativePath.startsWith("/") || segments2.some((segment) => segment === "." || segment === "..")) {
1763
+ throw new Error(`Unsafe checkout path: ${relativePath}`);
1764
+ }
1765
+ };
1766
+ var toRelativePath = (storePath) => {
1767
+ const relativePath = storePath.replace(/^\/+/, "");
1768
+ assertSafeRelativePath(relativePath);
1769
+ return relativePath;
1770
+ };
1771
+ var joinRelativePath = (...parts) => parts.flatMap((part) => part.split("/")).filter(Boolean).join("/");
1772
+ var addLeadingSlash = (relativePath) => relativePath ? `/${relativePath}` : "/";
1773
+ var stripSyncRuleTarget = (localRelativePath, rule) => {
1774
+ if (!rule.target) {
1775
+ return localRelativePath;
1776
+ }
1777
+ if (localRelativePath === rule.target) {
1778
+ return "";
1779
+ }
1780
+ if (!localRelativePath.startsWith(`${rule.target}/`)) {
1781
+ return null;
1782
+ }
1783
+ return localRelativePath.slice(rule.target.length + 1);
1784
+ };
1785
+ var mapLocalPathToSyncRuleRemotePath = (localStorePath, rule) => {
1786
+ const localRelativePath = toRelativePath(localStorePath);
1787
+ const ruleRelativePath = stripSyncRuleTarget(localRelativePath, rule);
1788
+ if (ruleRelativePath === null) {
1789
+ throw new Error(
1790
+ `Path is outside sync rule target ${rule.target || "."}: ${localRelativePath}`
1791
+ );
1792
+ }
1793
+ return addLeadingSlash(joinRelativePath(rule.path, ruleRelativePath));
1794
+ };
1795
+ var mapRemotePathToSyncRuleLocalPath = (remoteStorePath, rule) => {
1796
+ const remoteRelativePath = toRelativePath(remoteStorePath);
1797
+ if (rule.path && remoteRelativePath !== rule.path && !remoteRelativePath.startsWith(`${rule.path}/`)) {
1798
+ throw new Error(
1799
+ `Remote path is outside sync rule path ${rule.path}: ${remoteRelativePath}`
1800
+ );
1801
+ }
1802
+ const ruleRelativePath = rule.path ? remoteRelativePath === rule.path ? "" : remoteRelativePath.slice(rule.path.length + 1) : remoteRelativePath;
1803
+ return addLeadingSlash(joinRelativePath(rule.target, ruleRelativePath));
1804
+ };
1805
+
1806
+ // src/createCheckoutIgnoreMatcher.ts
1807
+ var createCheckoutIgnoreMatcher = (rule, sharedPatterns, personalPatterns) => {
1808
+ const matcher = ignore().add([...sharedPatterns, ...personalPatterns]);
1809
+ return {
1810
+ ignores: (candidate) => {
1811
+ const local = candidate.replace(/\/$/, "");
1812
+ if (rule.target && local !== rule.target && !local.startsWith(`${rule.target}/`))
1813
+ return false;
1814
+ const remote = mapLocalPathToSyncRuleRemotePath(local, rule).replace(
1815
+ /^\//,
1816
+ ""
1817
+ );
1818
+ return remote !== "" && matcher.ignores(candidate.endsWith("/") ? `${remote}/` : remote);
1819
+ }
1820
+ };
1821
+ };
1822
+
1823
+ // src/checkoutIgnore.ts
1824
+ import path3 from "path";
1825
+
1826
+ // src/sharedIgnoreFile.ts
1827
+ import path2 from "path";
1828
+ import { readFile } from "fs/promises";
1829
+
1830
+ // ../../packages/core/src/storeIgnore.ts
1831
+ var segments = (relativePath) => relativePath.split("/").filter(Boolean);
1832
+ var basename = (relativePath) => {
1833
+ const parts = segments(relativePath);
1834
+ return parts[parts.length - 1] ?? "";
1835
+ };
1836
+ var dirRule = (name) => ({
1837
+ label: name,
1838
+ pattern: `${name}/`,
1839
+ matches: (relativePath) => segments(relativePath).slice(0, -1).includes(name)
1840
+ });
1841
+ var ENV_EXAMPLE_SUFFIXES = [".example", ".sample", ".template", ".dist"];
1842
+ var envRule = {
1843
+ label: ".env",
1844
+ pattern: ".env",
1845
+ matches: (relativePath) => {
1846
+ const name = basename(relativePath);
1847
+ if (name !== ".env" && !name.startsWith(".env.")) {
1848
+ return false;
1849
+ }
1850
+ return !ENV_EXAMPLE_SUFFIXES.some((suffix) => name.endsWith(suffix));
1851
+ }
1852
+ };
1853
+ var logRule = {
1854
+ label: "*.log",
1855
+ pattern: "*.log",
1856
+ matches: (relativePath) => basename(relativePath).endsWith(".log")
1857
+ };
1858
+ var vitepressRule = {
1859
+ label: ".vitepress/cache",
1860
+ pattern: ".vitepress/cache/",
1861
+ matches: (relativePath) => relativePath.includes(".vitepress/cache/") || relativePath.includes(".vitepress/dist/")
1862
+ };
1863
+ var KNOWN_JUNK_RULES = [
1864
+ dirRule("node_modules"),
1865
+ dirRule("dist"),
1866
+ dirRule("build"),
1867
+ dirRule(".next"),
1868
+ dirRule(".nuxt"),
1869
+ dirRule(".svelte-kit"),
1870
+ dirRule(".turbo"),
1871
+ dirRule(".cache"),
1872
+ dirRule("coverage"),
1873
+ dirRule("target"),
1874
+ dirRule("__pycache__"),
1875
+ dirRule(".venv"),
1876
+ vitepressRule,
1877
+ envRule,
1878
+ logRule
1879
+ ];
1880
+ var detectJunk = (paths, rules = KNOWN_JUNK_RULES) => {
1881
+ const grouped = /* @__PURE__ */ new Map();
1882
+ for (const path22 of paths) {
1883
+ const rule = rules.find((candidate) => candidate.matches(path22));
1884
+ if (!rule) continue;
1885
+ const existing = grouped.get(rule);
1886
+ if (existing) existing.push(path22);
1887
+ else grouped.set(rule, [path22]);
1888
+ }
1889
+ return rules.filter((rule) => grouped.has(rule)).map((rule) => ({
1890
+ rule,
1891
+ paths: [...grouped.get(rule) ?? []].sort(
1892
+ (left, right) => left < right ? -1 : left > right ? 1 : 0
1893
+ )
1894
+ }));
1895
+ };
1896
+ var IGNORE_PROPOSAL_RULES = [
1897
+ { marker: "package.json", markerKind: "file", patterns: ["node_modules/"] },
1898
+ { marker: ".vitepress", markerKind: "dir", patterns: [".vitepress/cache/"] },
1899
+ { marker: "next.config.js", markerKind: "file", patterns: [".next/"] },
1900
+ { marker: "next.config.mjs", markerKind: "file", patterns: [".next/"] },
1901
+ { marker: "Cargo.toml", markerKind: "file", patterns: ["target/"] },
1902
+ {
1903
+ marker: "pyproject.toml",
1904
+ markerKind: "file",
1905
+ patterns: ["__pycache__/", ".venv/"]
1906
+ }
1907
+ ];
1908
+ var analyzeIgnoreSignals = ({
1909
+ presentPaths,
1910
+ presentMarkers,
1911
+ gitignorePatterns = [],
1912
+ rules = KNOWN_JUNK_RULES,
1913
+ proposalRules = IGNORE_PROPOSAL_RULES
1914
+ }) => {
1915
+ const findings = [];
1916
+ const seen = /* @__PURE__ */ new Set();
1917
+ const add = (finding) => {
1918
+ if (seen.has(finding.pattern)) return;
1919
+ seen.add(finding.pattern);
1920
+ findings.push(finding);
1921
+ };
1922
+ for (const group of detectJunk(presentPaths, rules)) {
1923
+ add({
1924
+ source: "present",
1925
+ severity: "imminent",
1926
+ label: group.rule.label,
1927
+ pattern: group.rule.pattern,
1928
+ matchedPaths: group.paths
1929
+ });
1930
+ }
1931
+ for (const rule of proposalRules) {
1932
+ if (!presentMarkers.has(rule.marker)) continue;
1933
+ for (const pattern of rule.patterns) {
1934
+ add({
1935
+ source: "marker",
1936
+ severity: "predicted",
1937
+ label: `${rule.marker} \u2192 ${pattern}`,
1938
+ pattern,
1939
+ matchedPaths: []
1940
+ });
1941
+ }
1942
+ }
1943
+ for (const pattern of gitignorePatterns) {
1944
+ if (!pattern.trim()) continue;
1945
+ findings.push({
1946
+ source: "gitignore",
1947
+ severity: "predicted",
1948
+ label: pattern,
1949
+ pattern,
1950
+ matchedPaths: []
1951
+ });
1952
+ }
1953
+ const maxSeverity = findings.some(
1954
+ (finding) => finding.severity === "imminent"
1955
+ ) ? "imminent" : findings.length > 0 ? "predicted" : null;
1956
+ return {
1957
+ findings,
1958
+ proposedPatterns: findings.map((finding) => finding.pattern),
1959
+ maxSeverity
1960
+ };
1961
+ };
1962
+ var parseLocalIgnoreFile = (text) => text.split(/\r?\n/).filter((line) => line.length > 0);
1963
+
1964
+ // src/atomicWriteTextFile.ts
1965
+ import path from "path";
1966
+ import { chmod, mkdir, rename, rm, writeFile } from "fs/promises";
1967
+ var atomicWriteTextFile = async (filePath, text, isExecutable = false) => {
1968
+ const parentDir = path.dirname(filePath);
1969
+ const tempPath = path.join(
1970
+ parentDir,
1971
+ `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
1972
+ );
1973
+ await mkdir(parentDir, { recursive: true });
1974
+ try {
1975
+ await writeFile(tempPath, text, "utf8");
1976
+ await chmod(tempPath, isExecutable ? 493 : 420);
1977
+ await rename(tempPath, filePath);
1978
+ } catch (error51) {
1979
+ await rm(tempPath, { force: true }).catch(() => {
1980
+ });
1981
+ throw error51;
1982
+ }
1983
+ };
1984
+
1985
+ // src/planSharedIgnore.ts
1986
+ var equal = (left, right) => JSON.stringify(left) === JSON.stringify(right);
1987
+ var planSharedIgnore = ({
1988
+ base,
1989
+ local,
1990
+ remote
1991
+ }) => {
1992
+ if (local === null) return { action: "restore", patterns: remote };
1993
+ if (equal(local, remote)) return { action: "unchanged", patterns: local };
1994
+ if (base === null) return { action: "conflict", patterns: local };
1995
+ if (equal(local, base)) return { action: "retrieve", patterns: remote };
1996
+ if (equal(remote, base)) return { action: "publish", patterns: local };
1997
+ return { action: "conflict", patterns: local };
1998
+ };
1999
+
2000
+ // src/sharedIgnoreFile.ts
2001
+ var readSharedIgnoreFile = async (filePath) => {
2002
+ try {
2003
+ return parseLocalIgnoreFile(await readFile(filePath, "utf8"));
2004
+ } catch (error51) {
2005
+ if (error51.code === "ENOENT") return null;
2006
+ throw error51;
2007
+ }
2008
+ };
2009
+ var writeSharedIgnoreFile = async (filePath, patterns) => {
2010
+ await atomicWriteTextFile(
2011
+ filePath,
2012
+ patterns.length ? `${patterns.join("\n")}
2013
+ ` : ""
2014
+ );
2015
+ };
2016
+ var readState = async (statePath) => {
2017
+ try {
2018
+ return JSON.parse(await readFile(statePath, "utf8"));
2019
+ } catch (error51) {
2020
+ if (error51.code === "ENOENT") return {};
2021
+ throw error51;
2022
+ }
2023
+ };
2024
+ var reconcileSharedIgnoreFile = async ({
2025
+ rootDir,
2026
+ sharedFilePath,
2027
+ store,
2028
+ remote,
2029
+ publish,
2030
+ dryRun = false
2031
+ }) => {
2032
+ const statePath = path2.join(rootDir, ".forum", "ignore-state.json");
2033
+ const state = await readState(statePath);
2034
+ const local = await readSharedIgnoreFile(sharedFilePath);
2035
+ const plan = planSharedIgnore({ base: state[store] ?? null, local, remote });
2036
+ if (dryRun || plan.action === "conflict") return plan;
2037
+ if (plan.action === "publish") await publish(plan.patterns);
2038
+ if (plan.action === "retrieve" || plan.action === "restore") {
2039
+ await writeSharedIgnoreFile(sharedFilePath, plan.patterns);
2040
+ }
2041
+ if (JSON.stringify(state[store]) !== JSON.stringify(plan.patterns)) {
2042
+ state[store] = plan.patterns;
2043
+ await atomicWriteTextFile(statePath, `${JSON.stringify(state, null, 2)}
2044
+ `);
2045
+ }
2046
+ return plan;
2047
+ };
2048
+
2049
+ // src/checkoutIgnore.ts
2050
+ var sharedIgnorePath = (rootDir, store) => path3.join(rootDir, ".forum", "stores", encodeURIComponent(store), "ignore");
2051
+ var prepareCheckoutIgnore = async ({
2052
+ rootDir,
2053
+ syncRules,
2054
+ client,
2055
+ dryRun = false
2056
+ }) => {
2057
+ const personal = await readSharedIgnoreFile(path3.join(rootDir, ".forum/usr_ignore")) ?? [];
2058
+ const patternsByStore = {};
2059
+ const changes = [];
2060
+ for (const rule of syncRules) {
2061
+ if (patternsByStore[rule.store]) continue;
2062
+ const storeRef = {
2063
+ workspaceSlug: rule.storeOwner,
2064
+ storeSlug: rule.storeSlug
2065
+ };
2066
+ const metadata = await client.readStoreByRef(storeRef);
2067
+ if (!metadata) throw new Error(`Store not found: ${rule.store}`);
2068
+ const sharedFilePath = sharedIgnorePath(rootDir, rule.store);
2069
+ const plan = await reconcileSharedIgnoreFile({
2070
+ rootDir,
2071
+ sharedFilePath,
2072
+ store: rule.store,
2073
+ remote: metadata.ignore ?? [],
2074
+ dryRun,
2075
+ publish: async (patterns) => {
2076
+ await client.syncStoreIgnoreByRef({
2077
+ ...storeRef,
2078
+ expectedIgnore: metadata.ignore ?? [],
2079
+ ignore: patterns
2080
+ });
2081
+ }
2082
+ });
2083
+ patternsByStore[rule.store] = [...plan.patterns, ...personal];
2084
+ if (plan.action !== "unchanged")
2085
+ changes.push({
2086
+ ...plan,
2087
+ relativePath: path3.relative(rootDir, sharedFilePath),
2088
+ store: rule.store
2089
+ });
2090
+ }
2091
+ const matchersByRule = {};
2092
+ for (const rule of syncRules) {
2093
+ const patterns = patternsByStore[rule.store];
2094
+ matchersByRule[rule.id] = createCheckoutIgnoreMatcher(
2095
+ rule,
2096
+ patterns.slice(0, patterns.length - personal.length),
2097
+ personal
2098
+ );
2099
+ }
2100
+ return { patternsByStore, changes, matchersByRule };
2101
+ };
2102
+
1756
2103
  // src/index.ts
1757
2104
  import { realpathSync, watch as watch2 } from "fs";
1758
- import path19 from "path";
2105
+ import path21 from "path";
1759
2106
  import {
1760
2107
  lstat as lstat2,
1761
- mkdir as mkdir5,
1762
- readFile as readFile7,
2108
+ mkdir as mkdir6,
2109
+ readFile as readFile8,
1763
2110
  rename as rename4,
1764
- rm as rm4,
2111
+ rm as rm5,
1765
2112
  writeFile as writeFile6
1766
2113
  } from "fs/promises";
1767
- import { createInterface } from "readline/promises";
2114
+ import { createInterface as createInterface2 } from "readline/promises";
1768
2115
  import { pathToFileURL } from "url";
1769
2116
  import { parse as parseYaml } from "yaml";
1770
2117
 
@@ -2534,10 +2881,10 @@ function mergeDefs(...defs) {
2534
2881
  function cloneDef(schema) {
2535
2882
  return mergeDefs(schema._zod.def);
2536
2883
  }
2537
- function getElementAtPath(obj, path20) {
2538
- if (!path20)
2884
+ function getElementAtPath(obj, path22) {
2885
+ if (!path22)
2539
2886
  return obj;
2540
- return path20.reduce((acc, key) => acc?.[key], obj);
2887
+ return path22.reduce((acc, key) => acc?.[key], obj);
2541
2888
  }
2542
2889
  function promiseAllObject(promisesObj) {
2543
2890
  const keys = Object.keys(promisesObj);
@@ -2946,11 +3293,11 @@ function explicitlyAborted(x, startIndex = 0) {
2946
3293
  }
2947
3294
  return false;
2948
3295
  }
2949
- function prefixIssues(path20, issues) {
3296
+ function prefixIssues(path22, issues) {
2950
3297
  return issues.map((iss) => {
2951
3298
  var _a3;
2952
3299
  (_a3 = iss).path ?? (_a3.path = []);
2953
- iss.path.unshift(path20);
3300
+ iss.path.unshift(path22);
2954
3301
  return iss;
2955
3302
  });
2956
3303
  }
@@ -3097,16 +3444,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
3097
3444
  }
3098
3445
  function formatError(error51, mapper = (issue2) => issue2.message) {
3099
3446
  const fieldErrors = { _errors: [] };
3100
- const processError = (error52, path20 = []) => {
3447
+ const processError = (error52, path22 = []) => {
3101
3448
  for (const issue2 of error52.issues) {
3102
3449
  if (issue2.code === "invalid_union" && issue2.errors.length) {
3103
- issue2.errors.map((issues) => processError({ issues }, [...path20, ...issue2.path]));
3450
+ issue2.errors.map((issues) => processError({ issues }, [...path22, ...issue2.path]));
3104
3451
  } else if (issue2.code === "invalid_key") {
3105
- processError({ issues: issue2.issues }, [...path20, ...issue2.path]);
3452
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
3106
3453
  } else if (issue2.code === "invalid_element") {
3107
- processError({ issues: issue2.issues }, [...path20, ...issue2.path]);
3454
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
3108
3455
  } else {
3109
- const fullpath = [...path20, ...issue2.path];
3456
+ const fullpath = [...path22, ...issue2.path];
3110
3457
  if (fullpath.length === 0) {
3111
3458
  fieldErrors._errors.push(mapper(issue2));
3112
3459
  } else {
@@ -3133,17 +3480,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
3133
3480
  }
3134
3481
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
3135
3482
  const result = { errors: [] };
3136
- const processError = (error52, path20 = []) => {
3483
+ const processError = (error52, path22 = []) => {
3137
3484
  var _a3, _b;
3138
3485
  for (const issue2 of error52.issues) {
3139
3486
  if (issue2.code === "invalid_union" && issue2.errors.length) {
3140
- issue2.errors.map((issues) => processError({ issues }, [...path20, ...issue2.path]));
3487
+ issue2.errors.map((issues) => processError({ issues }, [...path22, ...issue2.path]));
3141
3488
  } else if (issue2.code === "invalid_key") {
3142
- processError({ issues: issue2.issues }, [...path20, ...issue2.path]);
3489
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
3143
3490
  } else if (issue2.code === "invalid_element") {
3144
- processError({ issues: issue2.issues }, [...path20, ...issue2.path]);
3491
+ processError({ issues: issue2.issues }, [...path22, ...issue2.path]);
3145
3492
  } else {
3146
- const fullpath = [...path20, ...issue2.path];
3493
+ const fullpath = [...path22, ...issue2.path];
3147
3494
  if (fullpath.length === 0) {
3148
3495
  result.errors.push(mapper(issue2));
3149
3496
  continue;
@@ -3175,8 +3522,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
3175
3522
  }
3176
3523
  function toDotPath(_path) {
3177
3524
  const segs = [];
3178
- const path20 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3179
- for (const seg of path20) {
3525
+ const path22 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
3526
+ for (const seg of path22) {
3180
3527
  if (typeof seg === "number")
3181
3528
  segs.push(`[${seg}]`);
3182
3529
  else if (typeof seg === "symbol")
@@ -15868,13 +16215,13 @@ function resolveRef(ref, ctx) {
15868
16215
  if (!ref.startsWith("#")) {
15869
16216
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15870
16217
  }
15871
- const path20 = ref.slice(1).split("/").filter(Boolean);
15872
- if (path20.length === 0) {
16218
+ const path22 = ref.slice(1).split("/").filter(Boolean);
16219
+ if (path22.length === 0) {
15873
16220
  return ctx.rootSchema;
15874
16221
  }
15875
16222
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15876
- if (path20[0] === defsKey) {
15877
- const key = path20[1];
16223
+ if (path22[0] === defsKey) {
16224
+ const key = path22[1];
15878
16225
  if (!key || !ctx.defs[key]) {
15879
16226
  throw new Error(`Reference not found: ${ref}`);
15880
16227
  }
@@ -17212,8 +17559,8 @@ var normalizeStorePath = (value) => {
17212
17559
  }
17213
17560
  return segments2.length === 0 ? "/" : `/${segments2.join("/")}`;
17214
17561
  };
17215
- var storePathToArray = (path20) => {
17216
- const normalizedPath = normalizeStorePath(path20);
17562
+ var storePathToArray = (path22) => {
17563
+ const normalizedPath = normalizeStorePath(path22);
17217
17564
  return normalizedPath === "/" ? [] : normalizedPath.split("/").filter(Boolean);
17218
17565
  };
17219
17566
  var storePathSchema = external_exports.string().trim().min(1).transform((value) => normalizeStorePath(value));
@@ -18289,7 +18636,7 @@ var storeVisibilityValueSchema = external_exports.union([
18289
18636
  var storeVisibilitySchema = storeVisibilityValueSchema.default(
18290
18637
  WORKSPACE_STORE_VISIBILITY
18291
18638
  );
18292
- var storeIgnorePatternsSchema = external_exports.array(external_exports.string().trim().min(1));
18639
+ var storeIgnorePatternsSchema = external_exports.array(external_exports.string().min(1));
18293
18640
  var storeSearchSettingsSchema = external_exports.object({
18294
18641
  enabled: external_exports.boolean().default(false),
18295
18642
  semanticEnabled: external_exports.boolean().default(false),
@@ -18345,7 +18692,9 @@ var listSearchReconciliationStoresInputSchema = external_exports.object({
18345
18692
  limit: external_exports.number().int().min(1).max(500).default(100),
18346
18693
  touchedAfter: external_exports.string().datetime()
18347
18694
  });
18348
- var listOwnStoresInputSchema = external_exports.object({});
18695
+ var listOwnStoresInputSchema = external_exports.object({
18696
+ workspaceId: external_exports.string().trim().min(1).optional()
18697
+ });
18349
18698
  var readStoreMetadataInputSchema = external_exports.object({
18350
18699
  workspaceId: storeWorkspaceIdSchema,
18351
18700
  storeId: storeIdSchema
@@ -18432,10 +18781,15 @@ var pinnedStoreIdsValueSchema = external_exports.array(storePreferenceIdSchema);
18432
18781
  var recentStoresValueSchema = external_exports.array(recentStoreVisitSchema);
18433
18782
  var pinnedStoreIdsSchema = pinnedStoreIdsValueSchema.default([]);
18434
18783
  var recentStoresSchema = recentStoresValueSchema.default([]);
18784
+ var createTypeLastUsedAtSchema = external_exports.record(
18785
+ external_exports.string().min(1).max(100),
18786
+ external_exports.string().datetime()
18787
+ );
18435
18788
  var userPreferencesSchema = external_exports.object({
18436
18789
  id: storeOwnerIdSchema,
18437
18790
  pinnedStoreIds: pinnedStoreIdsSchema,
18438
18791
  recentStores: recentStoresSchema,
18792
+ createTypeLastUsedAt: createTypeLastUsedAtSchema.default({}),
18439
18793
  recentStoresEnabled: external_exports.boolean().default(true),
18440
18794
  showHomeWelcomeBanner: external_exports.boolean().default(true),
18441
18795
  showHiddenFiles: external_exports.boolean().default(false),
@@ -18455,6 +18809,7 @@ var userPreferencesSchema = external_exports.object({
18455
18809
  var userPreferencesUpdateSchema = external_exports.object({
18456
18810
  pinnedStoreIds: pinnedStoreIdsValueSchema,
18457
18811
  recentStores: recentStoresValueSchema,
18812
+ createTypeLastUsedAt: createTypeLastUsedAtSchema,
18458
18813
  recentStoresEnabled: external_exports.boolean(),
18459
18814
  showHomeWelcomeBanner: external_exports.boolean(),
18460
18815
  showHiddenFiles: external_exports.boolean(),
@@ -19187,21 +19542,21 @@ var isKey = (value) => /^\w*$/.test(value);
19187
19542
  var isUndefined = (val) => val === void 0;
19188
19543
  var compact = (value) => Array.isArray(value) ? value.filter(Boolean) : [];
19189
19544
  var stringToPath = (input) => compact(input.replace(/["|']|\]/g, "").split(/\.|\[/));
19190
- var get = (object2, path20, defaultValue) => {
19191
- if (!path20 || !isObject2(object2)) {
19545
+ var get = (object2, path22, defaultValue) => {
19546
+ if (!path22 || !isObject2(object2)) {
19192
19547
  return defaultValue;
19193
19548
  }
19194
- const paths = isKey(path20) ? [path20] : stringToPath(path20);
19549
+ const paths = isKey(path22) ? [path22] : stringToPath(path22);
19195
19550
  const result = paths.reduce((result2, key) => {
19196
19551
  return isNullOrUndefined(result2) ? void 0 : result2[key];
19197
19552
  }, object2);
19198
- return isUndefined(result) || result === object2 ? isUndefined(object2[path20]) ? defaultValue : object2[path20] : result;
19553
+ return isUndefined(result) || result === object2 ? isUndefined(object2[path22]) ? defaultValue : object2[path22] : result;
19199
19554
  };
19200
19555
  var isBoolean = (value) => typeof value === "boolean";
19201
19556
  var isFunction = (value) => typeof value === "function";
19202
- var set2 = (object2, path20, value) => {
19557
+ var set2 = (object2, path22, value) => {
19203
19558
  let index = -1;
19204
- const tempPath = isKey(path20) ? [path20] : stringToPath(path20);
19559
+ const tempPath = isKey(path22) ? [path22] : stringToPath(path22);
19205
19560
  const length = tempPath.length;
19206
19561
  const lastIndex = length - 1;
19207
19562
  while (++index < length) {
@@ -19399,12 +19754,12 @@ function isEmptyArray(obj) {
19399
19754
  }
19400
19755
  return true;
19401
19756
  }
19402
- function unset(object2, path20) {
19403
- if (isString(path20) && Object.prototype.hasOwnProperty.call(object2, path20)) {
19404
- delete object2[path20];
19757
+ function unset(object2, path22) {
19758
+ if (isString(path22) && Object.prototype.hasOwnProperty.call(object2, path22)) {
19759
+ delete object2[path22];
19405
19760
  return object2;
19406
19761
  }
19407
- const paths = Array.isArray(path20) ? path20 : isKey(path20) ? [path20] : stringToPath(path20);
19762
+ const paths = Array.isArray(path22) ? path22 : isKey(path22) ? [path22] : stringToPath(path22);
19408
19763
  const childObject = paths.length === 1 ? object2 : baseGet(object2, paths);
19409
19764
  const index = paths.length - 1;
19410
19765
  const key = paths[index];
@@ -23129,7 +23484,7 @@ var publicActionSubmitResultSchema = external_exports.unknown();
23129
23484
 
23130
23485
  // ../../packages/core/src/widgetRuntime.ts
23131
23486
  var widgetRuntimeOutputPathSchema = storePathSchema.refine(
23132
- (path20) => path20 !== "/",
23487
+ (path22) => path22 !== "/",
23133
23488
  {
23134
23489
  message: "Path must not be the root directory."
23135
23490
  }
@@ -23179,7 +23534,7 @@ var widgetRuntimeCompleteAttachmentUploadInputSchema = storeRefSchema.safeExtend
23179
23534
  treeVisibility: external_exports.enum(["visible", "hidden"]).optional(),
23180
23535
  uploadIntentId: external_exports.string().trim().min(1)
23181
23536
  });
23182
- var widgetRuntimeLinkPathSchema = external_exports.string().trim().min(1).max(2048).refine((path20) => path20.startsWith("/") && !path20.startsWith("//"), {
23537
+ var widgetRuntimeLinkPathSchema = external_exports.string().trim().min(1).max(2048).refine((path22) => path22.startsWith("/") && !path22.startsWith("//"), {
23183
23538
  message: "Link path must be a relative application path."
23184
23539
  });
23185
23540
  var widgetRuntimeCreateSigningLinkInputSchema = storeRefSchema.safeExtend({
@@ -23230,168 +23585,10 @@ var storeMemberSchema = external_exports.object({
23230
23585
  createdAt: external_exports.string().datetime().default(() => (/* @__PURE__ */ new Date()).toISOString())
23231
23586
  });
23232
23587
 
23233
- // ../../packages/core/src/storeIgnore.ts
23234
- var segments = (relativePath) => relativePath.split("/").filter(Boolean);
23235
- var basename = (relativePath) => {
23236
- const parts = segments(relativePath);
23237
- return parts[parts.length - 1] ?? "";
23238
- };
23239
- var dirRule = (name) => ({
23240
- label: name,
23241
- pattern: `${name}/`,
23242
- matches: (relativePath) => segments(relativePath).slice(0, -1).includes(name)
23243
- });
23244
- var ENV_EXAMPLE_SUFFIXES = [".example", ".sample", ".template", ".dist"];
23245
- var envRule = {
23246
- label: ".env",
23247
- pattern: ".env",
23248
- matches: (relativePath) => {
23249
- const name = basename(relativePath);
23250
- if (name !== ".env" && !name.startsWith(".env.")) {
23251
- return false;
23252
- }
23253
- return !ENV_EXAMPLE_SUFFIXES.some((suffix) => name.endsWith(suffix));
23254
- }
23255
- };
23256
- var logRule = {
23257
- label: "*.log",
23258
- pattern: "*.log",
23259
- matches: (relativePath) => basename(relativePath).endsWith(".log")
23260
- };
23261
- var vitepressRule = {
23262
- label: ".vitepress/cache",
23263
- pattern: ".vitepress/cache/",
23264
- matches: (relativePath) => relativePath.includes(".vitepress/cache/") || relativePath.includes(".vitepress/dist/")
23265
- };
23266
- var KNOWN_JUNK_RULES = [
23267
- dirRule("node_modules"),
23268
- dirRule("dist"),
23269
- dirRule("build"),
23270
- dirRule(".next"),
23271
- dirRule(".nuxt"),
23272
- dirRule(".svelte-kit"),
23273
- dirRule(".turbo"),
23274
- dirRule(".cache"),
23275
- dirRule("coverage"),
23276
- dirRule("target"),
23277
- dirRule("__pycache__"),
23278
- dirRule(".venv"),
23279
- vitepressRule,
23280
- envRule,
23281
- logRule
23282
- ];
23283
- var detectJunk = (paths, rules = KNOWN_JUNK_RULES) => {
23284
- const grouped = /* @__PURE__ */ new Map();
23285
- for (const path20 of paths) {
23286
- const rule = rules.find((candidate) => candidate.matches(path20));
23287
- if (!rule) continue;
23288
- const existing = grouped.get(rule);
23289
- if (existing) existing.push(path20);
23290
- else grouped.set(rule, [path20]);
23291
- }
23292
- return rules.filter((rule) => grouped.has(rule)).map((rule) => ({
23293
- rule,
23294
- paths: [...grouped.get(rule) ?? []].sort(
23295
- (left, right) => left < right ? -1 : left > right ? 1 : 0
23296
- )
23297
- }));
23298
- };
23299
- var IGNORE_PROPOSAL_RULES = [
23300
- { marker: "package.json", markerKind: "file", patterns: ["node_modules/"] },
23301
- { marker: ".vitepress", markerKind: "dir", patterns: [".vitepress/cache/"] },
23302
- { marker: "next.config.js", markerKind: "file", patterns: [".next/"] },
23303
- { marker: "next.config.mjs", markerKind: "file", patterns: [".next/"] },
23304
- { marker: "Cargo.toml", markerKind: "file", patterns: ["target/"] },
23305
- {
23306
- marker: "pyproject.toml",
23307
- markerKind: "file",
23308
- patterns: ["__pycache__/", ".venv/"]
23309
- }
23310
- ];
23311
- var analyzeIgnoreSignals = ({
23312
- presentPaths,
23313
- presentMarkers,
23314
- gitignorePatterns = [],
23315
- rules = KNOWN_JUNK_RULES,
23316
- proposalRules = IGNORE_PROPOSAL_RULES
23317
- }) => {
23318
- const findings = [];
23319
- const seen = /* @__PURE__ */ new Set();
23320
- const add = (finding) => {
23321
- if (seen.has(finding.pattern)) return;
23322
- seen.add(finding.pattern);
23323
- findings.push(finding);
23324
- };
23325
- for (const group of detectJunk(presentPaths, rules)) {
23326
- add({
23327
- source: "present",
23328
- severity: "imminent",
23329
- label: group.rule.label,
23330
- pattern: group.rule.pattern,
23331
- matchedPaths: group.paths
23332
- });
23333
- }
23334
- for (const rule of proposalRules) {
23335
- if (!presentMarkers.has(rule.marker)) continue;
23336
- for (const pattern of rule.patterns) {
23337
- add({
23338
- source: "marker",
23339
- severity: "predicted",
23340
- label: `${rule.marker} \u2192 ${pattern}`,
23341
- pattern,
23342
- matchedPaths: []
23343
- });
23344
- }
23345
- }
23346
- for (const pattern of gitignorePatterns) {
23347
- const trimmed = pattern.trim();
23348
- if (!trimmed) continue;
23349
- add({
23350
- source: "gitignore",
23351
- severity: "predicted",
23352
- label: trimmed,
23353
- pattern: trimmed,
23354
- matchedPaths: []
23355
- });
23356
- }
23357
- const maxSeverity = findings.some(
23358
- (finding) => finding.severity === "imminent"
23359
- ) ? "imminent" : findings.length > 0 ? "predicted" : null;
23360
- return {
23361
- findings,
23362
- proposedPatterns: findings.map((finding) => finding.pattern),
23363
- maxSeverity
23364
- };
23365
- };
23366
- var resolveIgnorePatterns = ({
23367
- bakedIn,
23368
- storeSetting,
23369
- localOverride
23370
- }) => {
23371
- const seen = /* @__PURE__ */ new Set();
23372
- const resolved = [];
23373
- for (const pattern of [...bakedIn, ...storeSetting, ...localOverride]) {
23374
- const trimmed = pattern.trim();
23375
- if (!trimmed || seen.has(trimmed)) continue;
23376
- seen.add(trimmed);
23377
- resolved.push(trimmed);
23378
- }
23379
- return resolved;
23380
- };
23381
- var parseLocalIgnoreFile = (text) => {
23382
- const patterns = [];
23383
- for (const rawLine of text.split(/\r?\n/)) {
23384
- const line = rawLine.trim();
23385
- if (!line || line.startsWith("#")) continue;
23386
- patterns.push(line);
23387
- }
23388
- return patterns;
23389
- };
23390
-
23391
23588
  // ../../packages/store-tree/src/patterns.ts
23392
23589
  var escapeRegex2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23393
- var basename2 = (path20) => {
23394
- const segments2 = path20.split("/").filter(Boolean);
23590
+ var basename2 = (path22) => {
23591
+ const segments2 = path22.split("/").filter(Boolean);
23395
23592
  return segments2[segments2.length - 1] ?? "";
23396
23593
  };
23397
23594
  var normalizeStorePathPattern = (pattern) => pattern.trim();
@@ -23409,7 +23606,7 @@ var storePathPatternToRegex = (pattern) => {
23409
23606
  return new RegExp(source, "i");
23410
23607
  };
23411
23608
  var doesStorePathPatternMatch = ({
23412
- path: path20,
23609
+ path: path22,
23413
23610
  pattern,
23414
23611
  mode = "path"
23415
23612
  }) => {
@@ -23417,7 +23614,7 @@ var doesStorePathPatternMatch = ({
23417
23614
  if (!normalizedPattern) {
23418
23615
  return true;
23419
23616
  }
23420
- const normalizedPath = normalizeStorePath(path20);
23617
+ const normalizedPath = normalizeStorePath(path22);
23421
23618
  const target = mode === "basename" ? basename2(normalizedPath) : normalizedPath;
23422
23619
  if (patternHasWildcard(normalizedPattern)) {
23423
23620
  return storePathPatternToRegex(normalizedPattern).test(target);
@@ -23449,9 +23646,21 @@ var storeRootPathFromId2 = (storeId) => {
23449
23646
  return `/${normalizeStoreId2(storeId)}`;
23450
23647
  };
23451
23648
 
23452
- // src/auth.ts
23649
+ // src/openInBrowser.ts
23453
23650
  import { execFile } from "child_process";
23454
23651
  import { promisify } from "util";
23652
+ var execFileAsync = promisify(execFile);
23653
+ var openInBrowser = async (url2) => {
23654
+ if (process.platform === "darwin") {
23655
+ await execFileAsync("open", [url2]);
23656
+ return;
23657
+ }
23658
+ if (process.platform === "win32") {
23659
+ await execFileAsync("cmd", ["/c", "start", "", url2]);
23660
+ return;
23661
+ }
23662
+ await execFileAsync("xdg-open", [url2]);
23663
+ };
23455
23664
 
23456
23665
  // src/render.ts
23457
23666
  var options = {
@@ -23493,8 +23702,8 @@ var isIssueRecord = (value) => {
23493
23702
  return isRecord2(value) && typeof value.message === "string";
23494
23703
  };
23495
23704
  var formatIssue = (issue2) => {
23496
- const path20 = Array.isArray(issue2.path) && issue2.path.length > 0 ? issue2.path.map(String).join(".") : "";
23497
- return path20 ? `${path20}: ${issue2.message}` : issue2.message;
23705
+ const path22 = Array.isArray(issue2.path) && issue2.path.length > 0 ? issue2.path.map(String).join(".") : "";
23706
+ return path22 ? `${path22}: ${issue2.message}` : issue2.message;
23498
23707
  };
23499
23708
  var formatIssueArray = (issues) => {
23500
23709
  if (!issues.every(isIssueRecord)) {
@@ -23775,19 +23984,28 @@ var withActivity = async (label, run) => {
23775
23984
  }
23776
23985
  };
23777
23986
 
23987
+ // src/waitForLoginBrowser.ts
23988
+ import { createInterface } from "readline";
23989
+ var waitForLoginBrowser = () => new Promise((resolve, reject) => {
23990
+ const prompt = createInterface({
23991
+ input: process.stdin,
23992
+ output: process.stderr
23993
+ });
23994
+ const cancel = () => {
23995
+ reject(new Error("Login cancelled. Run `fm auth login` again."));
23996
+ prompt.close();
23997
+ };
23998
+ prompt.once("SIGINT", cancel);
23999
+ prompt.once("close", cancel);
24000
+ prompt.question("Press Enter to open the browser... ", () => {
24001
+ prompt.removeListener("close", cancel);
24002
+ prompt.removeListener("SIGINT", cancel);
24003
+ prompt.close();
24004
+ resolve();
24005
+ });
24006
+ });
24007
+
23778
24008
  // src/auth.ts
23779
- var execFileAsync = promisify(execFile);
23780
- var openInBrowser = async (url2) => {
23781
- if (process.platform === "darwin") {
23782
- await execFileAsync("open", [url2]);
23783
- return;
23784
- }
23785
- if (process.platform === "win32") {
23786
- await execFileAsync("cmd", ["/c", "start", "", url2]);
23787
- return;
23788
- }
23789
- await execFileAsync("xdg-open", [url2]);
23790
- };
23791
24009
  var interactiveLogin = async ({
23792
24010
  apiBaseUrl,
23793
24011
  timeoutMs = 2 * 60 * 1e3,
@@ -23806,10 +24024,11 @@ var interactiveLogin = async ({
23806
24024
  throw new Error(`Failed to start auth flow: ${message}`);
23807
24025
  }
23808
24026
  const session = await startResponse.json();
23809
- renderOutput(`Open this URL: ${session.verificationUrl}`);
23810
- renderOutput(`Enter this code: ${session.userCode}`);
24027
+ renderOutput(`First, copy this code: ${session.userCode}`);
24028
+ renderOutput(`Open this URL and enter the code: ${session.verificationUrl}`);
23811
24029
  renderOutput(`This code expires at: ${session.expiresAt}`);
23812
- if (openBrowser) {
24030
+ if (openBrowser && process.stdin.isTTY && process.stderr.isTTY) {
24031
+ await waitForLoginBrowser();
23813
24032
  void openInBrowser(session.verificationUrl).catch(() => {
23814
24033
  renderOutput(
23815
24034
  "Could not open browser automatically. Open the URL manually."
@@ -23957,7 +24176,7 @@ var buildContentChangedConflictExplainer = (issues) => {
23957
24176
  body: [
23958
24177
  pathSubject(
23959
24178
  paths,
23960
- (path20) => `${path20} changed both locally and remotely.`,
24179
+ (path22) => `${path22} changed both locally and remotely.`,
23961
24180
  (count) => `${count} files changed both locally and remotely.`
23962
24181
  ),
23963
24182
  "Forum left these files alone because either overwrite direction could lose work."
@@ -23989,7 +24208,7 @@ var buildLocalDeleteRemoteChangeExplainer = (issues) => {
23989
24208
  body: [
23990
24209
  pathSubject(
23991
24210
  paths,
23992
- (path20) => `${path20} was deleted locally, but the remote file changed.`,
24211
+ (path22) => `${path22} was deleted locally, but the remote file changed.`,
23993
24212
  (count) => `${count} files were deleted locally, but changed remotely.`
23994
24213
  ),
23995
24214
  "Forum did not delete newer remote work automatically."
@@ -24021,7 +24240,7 @@ var buildMoveDestinationConflictExplainer = (issues) => {
24021
24240
  body: [
24022
24241
  pathSubject(
24023
24242
  paths,
24024
- (path20) => `${path20} is the destination of a remote move, but already exists locally.`,
24243
+ (path22) => `${path22} is the destination of a remote move, but already exists locally.`,
24025
24244
  (count) => `${count} paths are remote move destinations that already exist locally.`
24026
24245
  ),
24027
24246
  "Forum synced unrelated files but left these paths alone."
@@ -24056,7 +24275,7 @@ var buildMoveRemoteChangeConflictExplainer = (issues) => {
24056
24275
  body: [
24057
24276
  pathSubject(
24058
24277
  paths,
24059
- (path20) => `${path20} was involved in a move while the other side changed it.`,
24278
+ (path22) => `${path22} was involved in a move while the other side changed it.`,
24060
24279
  (count) => `${count} files were involved in moves while the other side changed them.`
24061
24280
  ),
24062
24281
  "Forum did not combine the move and content change automatically."
@@ -24088,7 +24307,7 @@ var buildMoveMoveConflictExplainer = (issues) => {
24088
24307
  body: [
24089
24308
  pathSubject(
24090
24309
  paths,
24091
- (path20) => `${path20} moved locally and remotely to different paths.`,
24310
+ (path22) => `${path22} moved locally and remotely to different paths.`,
24092
24311
  (count) => `${count} files moved locally and remotely to different paths.`
24093
24312
  ),
24094
24313
  "Forum left these moves unresolved because choosing a path automatically could hide work."
@@ -24120,7 +24339,7 @@ var buildDeleteMoveConflictExplainer = (issues) => {
24120
24339
  body: [
24121
24340
  pathSubject(
24122
24341
  paths,
24123
- (path20) => `${path20} was deleted on one side and moved on the other.`,
24342
+ (path22) => `${path22} was deleted on one side and moved on the other.`,
24124
24343
  (count) => `${count} files were deleted on one side and moved on the other.`
24125
24344
  ),
24126
24345
  "Forum did not choose between delete and move automatically."
@@ -24152,7 +24371,7 @@ var buildReferenceLocalFileConflictExplainer = (issues) => {
24152
24371
  body: [
24153
24372
  pathSubject(
24154
24373
  paths,
24155
- (path20) => `${path20} is now a remote reference, but the local file has changes.`,
24374
+ (path22) => `${path22} is now a remote reference, but the local file has changes.`,
24156
24375
  (count) => `${count} paths are now remote references, but local files have changes.`
24157
24376
  ),
24158
24377
  "Forum left the local files alone because replacing them would discard local work."
@@ -24184,7 +24403,7 @@ var buildUnsupportedLocalEntryExplainer = (issues) => {
24184
24403
  body: [
24185
24404
  pathSubject(
24186
24405
  paths,
24187
- (path20) => `${path20} is an unsupported local filesystem entry.`,
24406
+ (path22) => `${path22} is an unsupported local filesystem entry.`,
24188
24407
  (count) => `${count} local filesystem entries are unsupported.`
24189
24408
  ),
24190
24409
  "Forum skipped these paths and continued with unrelated files where it could."
@@ -24216,7 +24435,7 @@ var buildReadOnlyLocalChangeExplainer = (issues) => {
24216
24435
  body: [
24217
24436
  pathSubject(
24218
24437
  paths,
24219
- (path20) => `${path20} has local changes, but its sync rule is read-only.`,
24438
+ (path22) => `${path22} has local changes, but its sync rule is read-only.`,
24220
24439
  (count) => `${count} files have local changes, but their sync rules are read-only.`
24221
24440
  ),
24222
24441
  "Forum left these files alone so remote updates cannot overwrite local work."
@@ -24313,7 +24532,7 @@ var checkoutConflictIssuesFromResult = (result) => checkoutIssuesFromResult(resu
24313
24532
  (issue2) => issue2.type === "conflict"
24314
24533
  );
24315
24534
  var formatCheckoutExplainer = (explainer) => {
24316
- const pathLines = explainer.paths.length <= 1 ? [] : ["", ...formatPathList(explainer.paths).map((path20) => ` ${path20}`)];
24535
+ const pathLines = explainer.paths.length <= 1 ? [] : ["", ...formatPathList(explainer.paths).map((path22) => ` ${path22}`)];
24317
24536
  const actionLines = explainer.actions.map(
24318
24537
  (action) => `${styleText.bold(`${action.label}:`)} ${action.text}`
24319
24538
  );
@@ -24337,7 +24556,7 @@ var formatCheckoutExplainers = (result) => {
24337
24556
 
24338
24557
  // src/gitIntegration.ts
24339
24558
  import { execFile as execFile2 } from "child_process";
24340
- import path from "path";
24559
+ import path4 from "path";
24341
24560
  import { promisify as promisify2 } from "util";
24342
24561
  var execFileAsync2 = promisify2(execFile2);
24343
24562
  var gitErrorMessage = (error51) => {
@@ -24362,7 +24581,7 @@ var runGit = async (cwd, args) => {
24362
24581
  };
24363
24582
  var resolveGitRoot = async (cwd) => {
24364
24583
  const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
24365
- return path.resolve(cwd, result.stdout.trim());
24584
+ return path4.resolve(cwd, result.stdout.trim());
24366
24585
  };
24367
24586
  var readGitStatusEntries = async (gitRoot) => {
24368
24587
  const result = await runGit(gitRoot, [
@@ -24442,59 +24661,80 @@ var commitGitChanges = async (gitRoot, message) => {
24442
24661
  };
24443
24662
 
24444
24663
  // src/checkoutNaming.ts
24445
- import { existsSync } from "fs";
24446
- import path2 from "path";
24447
- var projectConfigFilenames = [
24448
- "forum.yaml",
24449
- "forum.yml",
24450
- "doctrine.yaml",
24451
- "doctrine.yml"
24452
- ];
24453
- var defaultProjectConfigFilename = "forum.yaml";
24454
- var checkoutStateDirectoryNames = [".forum", ".doctrine"];
24664
+ var defaultProjectConfigFilename = ".forum/sync.yaml";
24665
+ var projectConfigFilenames = [defaultProjectConfigFilename];
24666
+ var checkoutStateDirectoryNames = [".forum"];
24455
24667
  var defaultCheckoutStateDirectoryName = ".forum";
24456
- var resolveCheckoutStateDirectoryName = (rootDir) => {
24457
- for (const name of checkoutStateDirectoryNames) {
24458
- if (existsSync(path2.join(rootDir, name))) {
24459
- return name;
24668
+ var resolveCheckoutStateDirectoryName = () => defaultCheckoutStateDirectoryName;
24669
+ var isCheckoutStateDirectoryPath = (relativePath) => relativePath.split("/").includes(defaultCheckoutStateDirectoryName);
24670
+
24671
+ // src/resolveAmbiguousSyncMoves.ts
24672
+ import { rm as rm2 } from "fs/promises";
24673
+ import path5 from "path";
24674
+ var resolveAmbiguousSyncMoves = async ({
24675
+ conflictPolicy,
24676
+ dryRun,
24677
+ localPath,
24678
+ manifestFiles,
24679
+ snapshot,
24680
+ structuralPreflight
24681
+ }) => {
24682
+ const blocked = structuralPreflight.blockedDetails;
24683
+ if (!conflictPolicy || !blocked.length || !blocked.every((detail) => detail.conflict?.type === "ambiguous-local-move"))
24684
+ return [];
24685
+ const details = [];
24686
+ if (conflictPolicy === "remote") {
24687
+ for (const { relativePath } of blocked) {
24688
+ if (manifestFiles[relativePath]) {
24689
+ manifestFiles[relativePath] = {
24690
+ ...manifestFiles[relativePath],
24691
+ remoteRevisionNumber: null
24692
+ };
24693
+ } else {
24694
+ if (!dryRun)
24695
+ await rm2(path5.join(localPath, relativePath), { force: true });
24696
+ snapshot.localFileByPath.delete(relativePath);
24697
+ snapshot.candidatePaths = snapshot.candidatePaths.filter(
24698
+ (candidate) => candidate !== relativePath
24699
+ );
24700
+ details.push({ relativePath, status: "deleted" });
24701
+ }
24460
24702
  }
24461
24703
  }
24462
- return defaultCheckoutStateDirectoryName;
24704
+ structuralPreflight.blockedDetails = [];
24705
+ return details;
24463
24706
  };
24464
- var isCheckoutStateDirectoryPath = (relativePath) => checkoutStateDirectoryNames.some(
24465
- (name) => relativePath === name || relativePath.startsWith(`${name}/`)
24466
- );
24467
24707
 
24468
24708
  // src/checkout.ts
24469
24709
  import { createHash as createHash2 } from "crypto";
24470
- import path7 from "path";
24710
+ import path9 from "path";
24471
24711
  import {
24472
24712
  lstat,
24473
- mkdir as mkdir3,
24713
+ mkdir as mkdir4,
24474
24714
  readdir as readdir2,
24475
- readFile as readFile2,
24715
+ readFile as readFile3,
24476
24716
  rename as rename2,
24477
- rm as rm2,
24717
+ rm as rm3,
24478
24718
  stat,
24479
24719
  writeFile as writeFile3
24480
24720
  } from "fs/promises";
24481
24721
  import { TextDecoder as TextDecoder2 } from "util";
24482
- import ignore from "ignore";
24722
+ import ignore2 from "ignore";
24483
24723
 
24484
24724
  // src/buildCheckoutFileManifestEntry.ts
24485
- var toManifestRemotePath = (path20) => path20.startsWith("/") ? path20 : `/${path20}`;
24725
+ var toManifestRemotePath = (path22) => path22.startsWith("/") ? path22 : `/${path22}`;
24486
24726
  var buildCheckoutFileManifestEntry = ({
24487
24727
  storageClass,
24488
24728
  hash: hash2,
24489
24729
  manifestEntry,
24490
- path: path20,
24730
+ path: path22,
24491
24731
  remoteFile,
24492
24732
  source,
24493
24733
  syncedAt
24494
24734
  }) => {
24495
24735
  const remoteFileId = remoteFile.id ?? remoteFile._id ?? manifestEntry?.remoteFileId;
24496
24736
  if (!remoteFileId) {
24497
- throw new Error(`Remote file ${path20} did not include a file id`);
24737
+ throw new Error(`Remote file ${path22} did not include a file id`);
24498
24738
  }
24499
24739
  return {
24500
24740
  storageClass,
@@ -24502,7 +24742,7 @@ var buildCheckoutFileManifestEntry = ({
24502
24742
  isExecutable: storageClass === "text" ? remoteFile.isExecutable === true || void 0 : void 0,
24503
24743
  lastSyncedAt: syncedAt,
24504
24744
  remoteFileId,
24505
- remotePath: remoteFile.path ?? manifestEntry?.remotePath ?? toManifestRemotePath(path20),
24745
+ remotePath: remoteFile.path ?? manifestEntry?.remotePath ?? toManifestRemotePath(path22),
24506
24746
  remoteUpdatedAt: remoteFile.updatedAt ?? null,
24507
24747
  remoteRevisionNumber: remoteFile.revisionNumber ?? null,
24508
24748
  ...source ? { source } : {}
@@ -24604,14 +24844,14 @@ var buildSyncCheckoutSnapshot = ({
24604
24844
  };
24605
24845
 
24606
24846
  // src/checkoutDirectories.ts
24607
- import path3 from "path";
24608
- import { mkdir, readdir, rmdir } from "fs/promises";
24847
+ import path6 from "path";
24848
+ import { mkdir as mkdir2, readdir, rmdir } from "fs/promises";
24609
24849
  var parentDirectoriesForPath = (relativePath) => {
24610
24850
  const directories = [];
24611
- let directory = path3.posix.dirname(relativePath);
24851
+ let directory = path6.posix.dirname(relativePath);
24612
24852
  while (directory && directory !== ".") {
24613
24853
  directories.push(directory);
24614
- directory = path3.posix.dirname(directory);
24854
+ directory = path6.posix.dirname(directory);
24615
24855
  }
24616
24856
  return directories;
24617
24857
  };
@@ -24636,7 +24876,7 @@ var prepareCheckoutDirectories = async ({
24636
24876
  const directories = checkoutParentDirectories(relativePaths);
24637
24877
  await Promise.all(
24638
24878
  directories.map(
24639
- (directory) => mkdir(path3.join(localPath, directory), { recursive: true })
24879
+ (directory) => mkdir2(path6.join(localPath, directory), { recursive: true })
24640
24880
  )
24641
24881
  );
24642
24882
  return directories;
@@ -24650,7 +24890,7 @@ var cleanupCheckoutDirectories = async ({
24650
24890
  if (checkoutStateDirectoryNames.includes(directory)) {
24651
24891
  continue;
24652
24892
  }
24653
- const absolutePath = path3.join(localPath, directory);
24893
+ const absolutePath = path6.join(localPath, directory);
24654
24894
  try {
24655
24895
  const entries = await readdir(absolutePath);
24656
24896
  if (entries.length > 0) {
@@ -24673,13 +24913,13 @@ var cleanupCheckoutDirectories = async ({
24673
24913
  };
24674
24914
 
24675
24915
  // src/planRemoteDirectoryCleanup.ts
24676
- import path4 from "path";
24916
+ import path7 from "path";
24677
24917
  var ancestorDirectories = (relativePath) => {
24678
24918
  const directories = [];
24679
- let directory = path4.posix.dirname(relativePath);
24919
+ let directory = path7.posix.dirname(relativePath);
24680
24920
  while (directory && directory !== ".") {
24681
24921
  directories.push(directory);
24682
- directory = path4.posix.dirname(directory);
24922
+ directory = path7.posix.dirname(directory);
24683
24923
  }
24684
24924
  return directories;
24685
24925
  };
@@ -24696,7 +24936,7 @@ var planRemoteDirectoryCleanup = (removedPaths) => {
24696
24936
  };
24697
24937
 
24698
24938
  // src/cleanupRemoteCheckoutDirectories.ts
24699
- var addLeadingSlash = (relativePath) => relativePath ? `/${relativePath}` : "/";
24939
+ var addLeadingSlash2 = (relativePath) => relativePath ? `/${relativePath}` : "/";
24700
24940
  var cleanupRemoteCheckoutDirectories = async ({
24701
24941
  client,
24702
24942
  removedPaths,
@@ -24704,7 +24944,7 @@ var cleanupRemoteCheckoutDirectories = async ({
24704
24944
  }) => {
24705
24945
  const removed = [];
24706
24946
  for (const directory of planRemoteDirectoryCleanup(removedPaths)) {
24707
- const directoryPath = addLeadingSlash(directory);
24947
+ const directoryPath = addLeadingSlash2(directory);
24708
24948
  try {
24709
24949
  const listing = await client.listDirectoryByRef({
24710
24950
  ...storeRef,
@@ -24727,53 +24967,6 @@ var cleanupRemoteCheckoutDirectories = async ({
24727
24967
  return removed;
24728
24968
  };
24729
24969
 
24730
- // src/mapSyncRulePath.ts
24731
- var assertSafeRelativePath = (relativePath) => {
24732
- const segments2 = relativePath.split("/");
24733
- if (relativePath.startsWith("/") || segments2.some((segment) => segment === "." || segment === "..")) {
24734
- throw new Error(`Unsafe checkout path: ${relativePath}`);
24735
- }
24736
- };
24737
- var toRelativePath = (storePath) => {
24738
- const relativePath = storePath.replace(/^\/+/, "");
24739
- assertSafeRelativePath(relativePath);
24740
- return relativePath;
24741
- };
24742
- var joinRelativePath = (...parts) => parts.flatMap((part) => part.split("/")).filter(Boolean).join("/");
24743
- var addLeadingSlash2 = (relativePath) => relativePath ? `/${relativePath}` : "/";
24744
- var stripSyncRuleTarget = (localRelativePath, rule) => {
24745
- if (!rule.target) {
24746
- return localRelativePath;
24747
- }
24748
- if (localRelativePath === rule.target) {
24749
- return "";
24750
- }
24751
- if (!localRelativePath.startsWith(`${rule.target}/`)) {
24752
- return null;
24753
- }
24754
- return localRelativePath.slice(rule.target.length + 1);
24755
- };
24756
- var mapLocalPathToSyncRuleRemotePath = (localStorePath, rule) => {
24757
- const localRelativePath = toRelativePath(localStorePath);
24758
- const ruleRelativePath = stripSyncRuleTarget(localRelativePath, rule);
24759
- if (ruleRelativePath === null) {
24760
- throw new Error(
24761
- `Path is outside sync rule target ${rule.target || "."}: ${localRelativePath}`
24762
- );
24763
- }
24764
- return addLeadingSlash2(joinRelativePath(rule.path, ruleRelativePath));
24765
- };
24766
- var mapRemotePathToSyncRuleLocalPath = (remoteStorePath, rule) => {
24767
- const remoteRelativePath = toRelativePath(remoteStorePath);
24768
- if (rule.path && remoteRelativePath !== rule.path && !remoteRelativePath.startsWith(`${rule.path}/`)) {
24769
- throw new Error(
24770
- `Remote path is outside sync rule path ${rule.path}: ${remoteRelativePath}`
24771
- );
24772
- }
24773
- const ruleRelativePath = rule.path ? remoteRelativePath === rule.path ? "" : remoteRelativePath.slice(rule.path.length + 1) : remoteRelativePath;
24774
- return addLeadingSlash2(joinRelativePath(rule.target, ruleRelativePath));
24775
- };
24776
-
24777
24970
  // src/planSyncCheckoutPaths.ts
24778
24971
  var detailRelatedPaths = (detail) => [
24779
24972
  detail.relativePath,
@@ -24947,6 +25140,8 @@ var planRemoteRenameBeforePull = ({
24947
25140
 
24948
25141
  // src/planSyncCheckoutFile.ts
24949
25142
  var planSyncCheckoutFile = ({
25143
+ conflictPolicy,
25144
+ resolvedConflict,
24950
25145
  force,
24951
25146
  localIsExecutable = false,
24952
25147
  localHash,
@@ -24959,6 +25154,34 @@ var planSyncCheckoutFile = ({
24959
25154
  }) => {
24960
25155
  const hasLocal = localHash !== null;
24961
25156
  const hasManifest = manifestHash !== null;
25157
+ const localChanged = localHash !== manifestHash || localIsExecutable !== manifestIsExecutable;
25158
+ if (resolvedConflict && remoteKind === "file") {
25159
+ return {
25160
+ type: resolvedConflict === "remote" ? "pullRemote" : hasLocal ? "pushLocal" : "deleteRemote"
25161
+ };
25162
+ }
25163
+ if (conflictPolicy && remoteKind !== "reference") {
25164
+ if (!hasLocal && hasManifest && remoteKind === "file") {
25165
+ return {
25166
+ type: conflictPolicy === "local" || !remoteVersionChanged ? "deleteRemote" : "pullRemote"
25167
+ };
25168
+ }
25169
+ if (hasLocal && !hasManifest && (remoteKind === "file" || remotePathExists) && remoteHash !== localHash) {
25170
+ return { type: conflictPolicy === "local" ? "pushLocal" : "pullRemote" };
25171
+ }
25172
+ if (hasLocal && hasManifest && localChanged) {
25173
+ if (remoteKind === "missing" && !remotePathExists) {
25174
+ return {
25175
+ type: conflictPolicy === "local" ? "pushLocal" : "deleteLocal"
25176
+ };
25177
+ }
25178
+ if (remoteVersionChanged) {
25179
+ return {
25180
+ type: conflictPolicy === "local" ? "pushLocal" : "pullRemote"
25181
+ };
25182
+ }
25183
+ }
25184
+ }
24962
25185
  if (remoteKind === "reference") {
24963
25186
  if (!force && (hasManifest || hasLocal)) {
24964
25187
  return {
@@ -25014,7 +25237,6 @@ var planSyncCheckoutFile = ({
25014
25237
  message: "untracked local file differs from the existing remote file; use fm push --force to keep local (recommended) or fm pull --force to accept remote"
25015
25238
  };
25016
25239
  }
25017
- const localChanged = localHash !== manifestHash || localIsExecutable !== manifestIsExecutable;
25018
25240
  if (!force && localChanged && remoteVersionChanged) {
25019
25241
  return {
25020
25242
  type: "conflict",
@@ -25057,19 +25279,9 @@ var selectCheckoutFilePathsForSyncRule = ({
25057
25279
 
25058
25280
  // src/projectConfig.ts
25059
25281
  import { createHash } from "crypto";
25060
- import path5 from "path";
25061
- import { access, readFile, writeFile } from "fs/promises";
25282
+ import path8 from "path";
25283
+ import { access, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
25062
25284
  import { parse as parse3, stringify } from "yaml";
25063
- var warnedLegacyIgnorePaths = /* @__PURE__ */ new Set();
25064
- var warnLegacyIgnoreField = (filePath) => {
25065
- if (warnedLegacyIgnorePaths.has(filePath)) {
25066
- return;
25067
- }
25068
- warnedLegacyIgnorePaths.add(filePath);
25069
- renderWarning(
25070
- `ignore in ${filePath} is no longer used; move these to the store with \`fm ignore add\`, or to .forum/ignore`
25071
- );
25072
- };
25073
25285
  var fileExists = async (filePath) => {
25074
25286
  try {
25075
25287
  await access(filePath);
@@ -25102,7 +25314,9 @@ var parseProjectConfig = (filePath, source) => {
25102
25314
  );
25103
25315
  }
25104
25316
  if (ignore3 !== void 0) {
25105
- warnLegacyIgnoreField(filePath);
25317
+ throw new Error(
25318
+ `Invalid Forum config at ${filePath}: put shared ignore rules under .forum/stores/ or personal rules in .forum/usr_ignore.`
25319
+ );
25106
25320
  }
25107
25321
  return {
25108
25322
  ...defaultStore ? { defaultStore } : {},
@@ -25183,7 +25397,7 @@ var normalizeSyncPath = ({
25183
25397
  value
25184
25398
  }) => {
25185
25399
  const trimmed = value.trim();
25186
- if (path5.isAbsolute(trimmed)) {
25400
+ if (path8.isAbsolute(trimmed)) {
25187
25401
  throw new Error(
25188
25402
  `Invalid Forum config at ${filePath}: ${fieldName} must be a relative path.`
25189
25403
  );
@@ -25336,16 +25550,16 @@ var parseQualifiedConfigStore = (filePath, fieldName, store) => {
25336
25550
  return `${segments2[0]}/${segments2[1]}`;
25337
25551
  };
25338
25552
  var findForumProjectConfig = async (startDir = process.cwd()) => {
25339
- let current = path5.resolve(startDir);
25553
+ let current = path8.resolve(startDir);
25340
25554
  while (true) {
25341
25555
  for (const filename of projectConfigFilenames) {
25342
- const candidate = path5.join(current, filename);
25556
+ const candidate = path8.join(current, filename);
25343
25557
  if (await fileExists(candidate)) {
25344
- const source = await readFile(candidate, "utf8");
25558
+ const source = await readFile2(candidate, "utf8");
25345
25559
  return parseProjectConfig(candidate, source);
25346
25560
  }
25347
25561
  }
25348
- const parent = path5.dirname(current);
25562
+ const parent = path8.dirname(current);
25349
25563
  if (parent === current) {
25350
25564
  return null;
25351
25565
  }
@@ -25380,35 +25594,84 @@ var writeForumProjectConfig = async (filePath, config2) => {
25380
25594
  target: rule.target || "."
25381
25595
  }));
25382
25596
  }
25383
- await writeFile(filePath, stringify(value), "utf8");
25597
+ await mkdir3(path8.dirname(filePath), { recursive: true });
25598
+ await writeFile2(filePath, stringify(value), "utf8");
25384
25599
  };
25385
25600
 
25386
- // src/atomicWriteTextFile.ts
25387
- import path6 from "path";
25388
- import { chmod, mkdir as mkdir2, rename, rm, writeFile as writeFile2 } from "fs/promises";
25389
- var atomicWriteTextFile = async (filePath, text, isExecutable = false) => {
25390
- const parentDir = path6.dirname(filePath);
25391
- const tempPath = path6.join(
25392
- parentDir,
25393
- `.${path6.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
25394
- );
25395
- await mkdir2(parentDir, { recursive: true });
25601
+ // src/markdownStoreLinks.ts
25602
+ var markdownExtensions = /* @__PURE__ */ new Set([".md", ".markdown", ".mdx"]);
25603
+ var inlineLinkDestinationPattern = /(\]\()([^)\s]+)(\))/g;
25604
+ var normalizeStorePath2 = (value) => {
25605
+ const withoutFragment = value.split("#", 1)[0] ?? value;
25606
+ const withoutQuery = withoutFragment.split("?", 1)[0] ?? withoutFragment;
25607
+ return withoutQuery.startsWith("/") ? withoutQuery : `/${withoutQuery}`;
25608
+ };
25609
+ var normalizeFileExtension = (pathValue) => {
25610
+ const basename3 = pathValue.split("/").at(-1) ?? pathValue;
25611
+ const dotIndex = basename3.lastIndexOf(".");
25612
+ return dotIndex >= 0 ? basename3.slice(dotIndex).toLowerCase() : "";
25613
+ };
25614
+ var isMarkdownStorePath = (pathValue) => markdownExtensions.has(normalizeFileExtension(pathValue));
25615
+ var encodeRawFileUrl = (baseUrl, fileId) => `${baseUrl.replace(/\/+$/, "")}/raw-file/${encodeURIComponent(fileId)}`;
25616
+ var decodeRawFileId = (href) => {
25617
+ let url2;
25396
25618
  try {
25397
- await writeFile2(tempPath, text, "utf8");
25398
- await chmod(tempPath, isExecutable ? 493 : 420);
25399
- await rename(tempPath, filePath);
25400
- } catch (error51) {
25401
- await rm(tempPath, { force: true }).catch(() => {
25402
- });
25403
- throw error51;
25619
+ url2 = new URL(href);
25620
+ } catch {
25621
+ return null;
25622
+ }
25623
+ const segments2 = url2.pathname.split("/").filter(Boolean);
25624
+ if (segments2.length !== 2 || segments2[0] !== "raw-file") {
25625
+ return null;
25626
+ }
25627
+ try {
25628
+ return decodeURIComponent(segments2[1] ?? "");
25629
+ } catch {
25630
+ return null;
25404
25631
  }
25405
25632
  };
25633
+ var rewriteMarkdownLinkDestinations = (markdown, rewriteDestination) => markdown.replace(
25634
+ inlineLinkDestinationPattern,
25635
+ (_match, prefix, destination, suffix) => `${prefix}${rewriteDestination(destination)}${suffix}`
25636
+ );
25637
+ var rewriteStorePathLinksToRawUrls = ({
25638
+ rawFileBaseUrl: rawFileBaseUrl2,
25639
+ targets,
25640
+ text
25641
+ }) => {
25642
+ const targetByStorePath = new Map(
25643
+ targets.map((target) => [normalizeStorePath2(target.storePath), target])
25644
+ );
25645
+ return rewriteMarkdownLinkDestinations(text, (destination) => {
25646
+ if (!destination.startsWith("/") || destination.startsWith("//")) {
25647
+ return destination;
25648
+ }
25649
+ const target = targetByStorePath.get(normalizeStorePath2(destination));
25650
+ return target ? encodeRawFileUrl(rawFileBaseUrl2, target.fileId) : destination;
25651
+ });
25652
+ };
25653
+ var rewriteRawUrlsToStorePathLinks = ({
25654
+ targets,
25655
+ text
25656
+ }) => {
25657
+ const storePathByFileId = new Map(
25658
+ targets.map((target) => [
25659
+ target.fileId,
25660
+ normalizeStorePath2(target.storePath)
25661
+ ])
25662
+ );
25663
+ return rewriteMarkdownLinkDestinations(text, (destination) => {
25664
+ const fileId = decodeRawFileId(destination);
25665
+ if (!fileId) {
25666
+ return destination;
25667
+ }
25668
+ return storePathByFileId.get(fileId) ?? destination;
25669
+ });
25670
+ };
25406
25671
 
25407
25672
  // src/checkout.ts
25408
- var reservedRootFiles = new Set(projectConfigFilenames);
25409
- var reservedRootFileNames = [...reservedRootFiles];
25410
25673
  var checkoutFilesManifestFilename = "files.json";
25411
- var localIgnoreFilename = "ignore";
25674
+ var localIgnoreFilename = "usr_ignore";
25412
25675
  var gitDirectoryName = ".git";
25413
25676
  var defaultConcurrency = 8;
25414
25677
  var utf8Decoder = new TextDecoder2("utf-8", { fatal: true });
@@ -25542,18 +25805,49 @@ var checkoutEchoSuppressionPathsForWorkerValue = (value) => {
25542
25805
  };
25543
25806
  var compareRelativePath3 = (left, right) => left < right ? -1 : left > right ? 1 : 0;
25544
25807
  var remoteEntrySource = (entry) => entry.__forumSource;
25545
- var isReservedCheckoutFile = (relativePath) => {
25546
- const filename = relativePath.split("/").at(-1) ?? relativePath;
25547
- return reservedRootFiles.has(filename);
25548
- };
25549
- var isReservedCheckoutMetadataPath = (relativePath) => isReservedCheckoutFile(relativePath) || isCheckoutStateDirectoryPath(relativePath) || // `.git/` is the checkout's own VCS metadata — structurally never content,
25808
+ var remoteEntryStorePath = (entry) => entry.__forumStorePath ?? entry.path;
25809
+ var markdownLinkTargetsFromEntries = (entries) => entries.map(
25810
+ ({ entry }) => entry.id ? {
25811
+ fileId: entry.id,
25812
+ storePath: remoteEntryStorePath(entry)
25813
+ } : null
25814
+ ).filter((target) => Boolean(target));
25815
+ var markdownLinkTargetsFromManifest = (manifest) => Object.values(manifest.files).map(
25816
+ (entry) => entry.remoteFileId && entry.remotePath ? {
25817
+ fileId: entry.remoteFileId,
25818
+ storePath: entry.remotePath
25819
+ } : null
25820
+ ).filter((target) => Boolean(target));
25821
+ var rawFileBaseUrl = (client) => {
25822
+ const probeId = "__forum_probe__";
25823
+ return client.createRawFileUrl(probeId).replace(new RegExp(`/raw-file/${probeId}$`), "");
25824
+ };
25825
+ var rewriteMarkdownForLocalCheckout = ({
25826
+ client,
25827
+ relativePath,
25828
+ targets,
25829
+ text
25830
+ }) => isMarkdownStorePath(relativePath) && text.includes("](/") ? rewriteStorePathLinksToRawUrls({
25831
+ rawFileBaseUrl: rawFileBaseUrl(client),
25832
+ targets,
25833
+ text
25834
+ }) : text;
25835
+ var rewriteMarkdownForRemoteStore = ({
25836
+ manifest,
25837
+ relativePath,
25838
+ text
25839
+ }) => isMarkdownStorePath(relativePath) ? rewriteRawUrlsToStorePathLinks({
25840
+ targets: markdownLinkTargetsFromManifest(manifest),
25841
+ text
25842
+ }) : text;
25843
+ var isReservedCheckoutMetadataPath = (relativePath) => isCheckoutStateDirectoryPath(relativePath) || // `.git/` is the checkout's own VCS metadata — structurally never content,
25550
25844
  // like `.forum/`. It is reserved (not an ignore *rule*): there are no
25551
25845
  // baked-in ignore patterns, but tooling directories are still never synced.
25552
25846
  relativePath === gitDirectoryName || relativePath.startsWith(`${gitDirectoryName}/`);
25553
25847
  var reservedCheckoutMetadataMessage = "reserved checkout metadata path; ignored by local sync";
25554
25848
  var assertSafeRelativePath2 = (relativePath) => {
25555
25849
  const segments2 = relativePath.split("/");
25556
- if (path7.isAbsolute(relativePath) || segments2.some((segment) => segment === "." || segment === "..")) {
25850
+ if (path9.isAbsolute(relativePath) || segments2.some((segment) => segment === "." || segment === "..")) {
25557
25851
  throw new Error(`Unsafe checkout path: ${relativePath}`);
25558
25852
  }
25559
25853
  };
@@ -25708,6 +26002,7 @@ var createSyncRuleClient = (client, rule) => {
25708
26002
  };
25709
26003
  const mapListingEntries = (entries, remoteRoot) => entries.map((entry) => ({
25710
26004
  ...entry,
26005
+ __forumStorePath: entry.path,
25711
26006
  ...remoteRoot.displayStore ? {
25712
26007
  __forumRemoteRef: {
25713
26008
  ...remoteRoot.displayStore,
@@ -25866,8 +26161,8 @@ var createSyncRuleClient = (client, rule) => {
25866
26161
  }
25867
26162
  };
25868
26163
  };
25869
- var syncRuleFileParentPath = (rule) => path7.posix.dirname(addLeadingSlash3(rule.target));
25870
- var syncRuleRemoteFileParentPath = (rule) => path7.posix.dirname(addLeadingSlash3(rule.path));
26164
+ var syncRuleFileParentPath = (rule) => path9.posix.dirname(addLeadingSlash3(rule.target));
26165
+ var syncRuleRemoteFileParentPath = (rule) => path9.posix.dirname(addLeadingSlash3(rule.path));
25871
26166
  var assertSyncRuleFilePath = (storePath, rule) => {
25872
26167
  const relativePath = toRelativePath2(storePath);
25873
26168
  if (relativePath !== rule.target) {
@@ -25928,6 +26223,7 @@ var createSyncRuleFileClient = (client, rule) => {
25928
26223
  ...listing,
25929
26224
  entries: listing.entries.filter((entry) => entry.path === remotePath).map((entry) => ({
25930
26225
  ...entry,
26226
+ __forumStorePath: entry.path,
25931
26227
  path: localPathForEntry
25932
26228
  }))
25933
26229
  };
@@ -25945,6 +26241,7 @@ var createSyncRuleFileClient = (client, rule) => {
25945
26241
  ...listing,
25946
26242
  entries: listing.entries.filter((entry) => entry.path === remotePath).map((entry) => ({
25947
26243
  ...entry,
26244
+ __forumStorePath: entry.path,
25948
26245
  path: localPathForEntry
25949
26246
  }))
25950
26247
  };
@@ -25967,7 +26264,7 @@ var classifySyncRuleRemoteRoot = async (client, storeRef, rule) => {
25967
26264
  const listing = await client.listDirectoryByRef({
25968
26265
  ...storeRef,
25969
26266
  includeHidden: rule.includeHidden,
25970
- directoryPath: path7.posix.dirname(remotePath)
26267
+ directoryPath: path9.posix.dirname(remotePath)
25971
26268
  });
25972
26269
  const entry = listing.entries.find(
25973
26270
  (candidate) => candidate.path === remotePath
@@ -25991,7 +26288,7 @@ var classifySyncRuleLocalTarget = async (rootDir, rule) => {
25991
26288
  return "directory";
25992
26289
  }
25993
26290
  try {
25994
- const details = await lstat(path7.join(rootDir, rule.target));
26291
+ const details = await lstat(path9.join(rootDir, rule.target));
25995
26292
  if (details.isFile()) {
25996
26293
  return "file";
25997
26294
  }
@@ -26113,34 +26410,32 @@ var normalizeCheckoutIncludePaths = (includePaths) => {
26113
26410
  }
26114
26411
  return normalized;
26115
26412
  };
26116
- var checkoutFilesManifestPath = (rootDir) => path7.join(
26413
+ var checkoutFilesManifestPath = (rootDir) => path9.join(
26117
26414
  rootDir,
26118
- resolveCheckoutStateDirectoryName(rootDir),
26415
+ resolveCheckoutStateDirectoryName(),
26119
26416
  checkoutFilesManifestFilename
26120
26417
  );
26121
26418
  var hashBytes = (bytes) => `sha256:${createHash2("sha256").update(bytes).digest("hex")}`;
26122
26419
  var hashText = (text) => hashBytes(Buffer.from(text, "utf8"));
26123
26420
  var hashLocalFile = async (filePath) => {
26124
- return hashBytes(await readFile2(filePath));
26421
+ return hashBytes(await readFile3(filePath));
26125
26422
  };
26126
26423
  var isExecutableMode = (mode) => (mode & 73) !== 0;
26127
26424
  var emptyCheckoutFilesManifest = () => ({
26128
26425
  files: {},
26129
26426
  references: {},
26130
26427
  syncRules: {},
26131
- ignorePatterns: [],
26132
26428
  ignoreOnboardedAt: null,
26133
26429
  version: 1
26134
26430
  });
26135
26431
  var readCheckoutFilesManifest = async (rootDir) => {
26136
26432
  try {
26137
- const raw = await readFile2(checkoutFilesManifestPath(rootDir), "utf8");
26433
+ const raw = await readFile3(checkoutFilesManifestPath(rootDir), "utf8");
26138
26434
  const parsed = JSON.parse(raw);
26139
26435
  return {
26140
26436
  files: parsed.files ?? {},
26141
26437
  references: parsed.references ?? {},
26142
26438
  syncRules: parsed.syncRules ?? {},
26143
- ignorePatterns: parsed.ignorePatterns ?? [],
26144
26439
  ignoreOnboardedAt: parsed.ignoreOnboardedAt ?? null,
26145
26440
  version: 1
26146
26441
  };
@@ -26153,12 +26448,12 @@ var readCheckoutFilesManifest = async (rootDir) => {
26153
26448
  };
26154
26449
  var writeCheckoutFilesManifest = async (rootDir, manifest) => {
26155
26450
  const filePath = checkoutFilesManifestPath(rootDir);
26156
- const parentDir = path7.dirname(filePath);
26157
- const tempPath = path7.join(
26451
+ const parentDir = path9.dirname(filePath);
26452
+ const tempPath = path9.join(
26158
26453
  parentDir,
26159
26454
  `.${checkoutFilesManifestFilename}.${process.pid}.${Date.now()}.tmp`
26160
26455
  );
26161
- await mkdir3(parentDir, { recursive: true });
26456
+ await mkdir4(parentDir, { recursive: true });
26162
26457
  try {
26163
26458
  await writeFile3(
26164
26459
  tempPath,
@@ -26179,9 +26474,6 @@ var writeCheckoutFilesManifest = async (rootDir, manifest) => {
26179
26474
  ([leftId], [rightId]) => leftId < rightId ? -1 : leftId > rightId ? 1 : 0
26180
26475
  )
26181
26476
  ),
26182
- ignorePatterns: [...manifest.ignorePatterns].sort(
26183
- (left, right) => left < right ? -1 : left > right ? 1 : 0
26184
- ),
26185
26477
  ignoreOnboardedAt: manifest.ignoreOnboardedAt,
26186
26478
  version: 1
26187
26479
  },
@@ -26193,7 +26485,7 @@ var writeCheckoutFilesManifest = async (rootDir, manifest) => {
26193
26485
  );
26194
26486
  await rename2(tempPath, filePath);
26195
26487
  } catch (error51) {
26196
- await rm2(tempPath, { force: true }).catch(() => {
26488
+ await rm3(tempPath, { force: true }).catch(() => {
26197
26489
  });
26198
26490
  throw error51;
26199
26491
  }
@@ -26244,7 +26536,7 @@ var readFileSnapshot = async (filePath) => {
26244
26536
  };
26245
26537
  var isFileSnapshotEqual = (left, right) => left.size === right.size && left.mtimeMs === right.mtimeMs;
26246
26538
  var createIgnoreMatcher = (patterns) => {
26247
- const matcher = ignore();
26539
+ const matcher = ignore2();
26248
26540
  matcher.add(patterns);
26249
26541
  return matcher;
26250
26542
  };
@@ -26298,7 +26590,7 @@ var deleteLocalFileIfPresent = async (filePath) => {
26298
26590
  if (!details.isFile()) {
26299
26591
  return false;
26300
26592
  }
26301
- await rm2(filePath, { force: true });
26593
+ await rm3(filePath, { force: true });
26302
26594
  return true;
26303
26595
  } catch (error51) {
26304
26596
  if (isFileNotFoundError(error51)) {
@@ -26309,7 +26601,7 @@ var deleteLocalFileIfPresent = async (filePath) => {
26309
26601
  };
26310
26602
  var moveLocalFileIfPresent = async (sourcePath, destinationPath) => {
26311
26603
  try {
26312
- await mkdir3(path7.dirname(destinationPath), { recursive: true });
26604
+ await mkdir4(path9.dirname(destinationPath), { recursive: true });
26313
26605
  await rename2(sourcePath, destinationPath);
26314
26606
  return true;
26315
26607
  } catch (error51) {
@@ -26321,7 +26613,7 @@ var moveLocalFileIfPresent = async (sourcePath, destinationPath) => {
26321
26613
  };
26322
26614
  var readLocalFileHashIfPresent = async (rootDir, relativePath) => {
26323
26615
  try {
26324
- return await hashLocalFile(path7.join(rootDir, relativePath));
26616
+ return await hashLocalFile(path9.join(rootDir, relativePath));
26325
26617
  } catch (error51) {
26326
26618
  if (isFileNotFoundError(error51)) {
26327
26619
  return null;
@@ -26380,7 +26672,7 @@ var reconcileTrackedRemoteDelete = async ({
26380
26672
  return localFileHash !== null ? "deleted" : "ignored";
26381
26673
  }
26382
26674
  const deletedLocalFile = await deleteLocalFileIfPresent(
26383
- path7.join(localPath, relativePath)
26675
+ path9.join(localPath, relativePath)
26384
26676
  );
26385
26677
  delete manifest.files[relativePath];
26386
26678
  return deletedLocalFile ? "deleted" : "ignored";
@@ -26460,20 +26752,20 @@ var readOnlyCheckoutChangeResults = (changes) => changes.map((change) => ({
26460
26752
  status: "failed"
26461
26753
  }));
26462
26754
  var resetCheckoutSyncState = async (rootDir) => {
26463
- await rm2(checkoutFilesManifestPath(rootDir), { force: true });
26755
+ await rm3(checkoutFilesManifestPath(rootDir), { force: true });
26464
26756
  };
26465
26757
  var atomicWriteBinaryFile = async (filePath, bytes) => {
26466
- const parentDir = path7.dirname(filePath);
26467
- const tempPath = path7.join(
26758
+ const parentDir = path9.dirname(filePath);
26759
+ const tempPath = path9.join(
26468
26760
  parentDir,
26469
- `.${path7.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
26761
+ `.${path9.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
26470
26762
  );
26471
- await mkdir3(parentDir, { recursive: true });
26763
+ await mkdir4(parentDir, { recursive: true });
26472
26764
  try {
26473
26765
  await writeFile3(tempPath, bytes);
26474
26766
  await rename2(tempPath, filePath);
26475
26767
  } catch (error51) {
26476
- await rm2(tempPath, { force: true }).catch(() => {
26768
+ await rm3(tempPath, { force: true }).catch(() => {
26477
26769
  });
26478
26770
  throw error51;
26479
26771
  }
@@ -26511,7 +26803,7 @@ var readStableCheckoutFile = async (absolutePath, relativePath, attempts = 3) =>
26511
26803
  for (let attempt = 0; attempt < attempts; attempt += 1) {
26512
26804
  try {
26513
26805
  const before = await readFileSnapshot(absolutePath);
26514
- const buffer = await readFile2(absolutePath);
26806
+ const buffer = await readFile3(absolutePath);
26515
26807
  const after = await readFileSnapshot(absolutePath);
26516
26808
  if (!isFileSnapshotEqual(before, after)) {
26517
26809
  sawUnstableRead = true;
@@ -26571,8 +26863,8 @@ var readStableCheckoutFile = async (absolutePath, relativePath, attempts = 3) =>
26571
26863
  var walkLocalCheckoutFiles = async (rootDir, directoryPath, includePaths, matcher, scan) => {
26572
26864
  const entries = await readdir2(directoryPath, { withFileTypes: true });
26573
26865
  for (const entry of entries) {
26574
- const absolutePath = path7.join(directoryPath, entry.name);
26575
- const relativePath = path7.relative(rootDir, absolutePath).split(path7.sep).join("/");
26866
+ const absolutePath = path9.join(directoryPath, entry.name);
26867
+ const relativePath = path9.relative(rootDir, absolutePath).split(path9.sep).join("/");
26576
26868
  const classification = classifyCheckoutPath(
26577
26869
  relativePath,
26578
26870
  entry.isDirectory(),
@@ -26670,7 +26962,7 @@ var readRemoteCheckoutEntry = async (client, storeRef, relativePath) => {
26670
26962
  const storePath = toStorePath2(relativePath);
26671
26963
  const listing = await client.listDirectoryByRef({
26672
26964
  ...storeRef,
26673
- directoryPath: path7.posix.dirname(storePath)
26965
+ directoryPath: path9.posix.dirname(storePath)
26674
26966
  });
26675
26967
  const entry = listing.entries.find(
26676
26968
  (candidate) => candidate.path === storePath
@@ -26684,7 +26976,7 @@ var readRemoteStoreEntry = async (client, storeRef, relativePath) => {
26684
26976
  const storePath = toStorePath2(relativePath);
26685
26977
  const listing = await client.listDirectoryByRef({
26686
26978
  ...storeRef,
26687
- directoryPath: path7.posix.dirname(storePath)
26979
+ directoryPath: path9.posix.dirname(storePath)
26688
26980
  });
26689
26981
  return listing.entries.find(
26690
26982
  (candidate) => candidate.path === storePath
@@ -26708,7 +27000,7 @@ var readReferenceSourceForPath = async (client, storeRef, relativePath) => {
26708
27000
  return void 0;
26709
27001
  };
26710
27002
  var readLocalCheckoutFile = async (rootDir, relativePath) => {
26711
- const absolutePath = path7.join(rootDir, relativePath);
27003
+ const absolutePath = path9.join(rootDir, relativePath);
26712
27004
  let details;
26713
27005
  try {
26714
27006
  details = await lstat(absolutePath);
@@ -27158,7 +27450,7 @@ var joinDirectRootPath = (rootPath, relativePath) => {
27158
27450
  if (!relativePath) {
27159
27451
  return rootPath;
27160
27452
  }
27161
- return path7.posix.join(rootPath, relativePath);
27453
+ return path9.posix.join(rootPath, relativePath);
27162
27454
  };
27163
27455
  var directStorePath = (relativePath) => relativePath ? toStorePath2(relativePath) : "/";
27164
27456
  var relativePathFromDirectRoot = (rootPath, storePath) => {
@@ -27167,7 +27459,7 @@ var relativePathFromDirectRoot = (rootPath, storePath) => {
27167
27459
  return relativePath;
27168
27460
  }
27169
27461
  if (relativePath === rootPath) {
27170
- return path7.posix.basename(rootPath);
27462
+ return path9.posix.basename(rootPath);
27171
27463
  }
27172
27464
  const prefix = `${rootPath}/`;
27173
27465
  if (!relativePath.startsWith(prefix)) {
@@ -27215,22 +27507,26 @@ var updateFileManifestEntry = ({
27215
27507
  hash: hash2,
27216
27508
  manifest,
27217
27509
  manifestEntry,
27218
- path: path20,
27510
+ path: path22,
27219
27511
  remoteFile,
27220
27512
  source,
27221
27513
  syncedAt
27222
27514
  }) => {
27223
- manifest.files[path20] = buildCheckoutFileManifestEntry({
27515
+ manifest.files[path22] = buildCheckoutFileManifestEntry({
27224
27516
  storageClass,
27225
27517
  hash: hash2,
27226
27518
  manifestEntry,
27227
- path: path20,
27519
+ path: path22,
27228
27520
  remoteFile,
27229
27521
  source,
27230
27522
  syncedAt
27231
27523
  });
27232
27524
  };
27233
27525
  var reconcileRemoteRenamesBeforePull = async ({
27526
+ client,
27527
+ conflictPolicy,
27528
+ localFiles,
27529
+ storeRef,
27234
27530
  dryRun,
27235
27531
  files,
27236
27532
  force,
@@ -27242,7 +27538,8 @@ var reconcileRemoteRenamesBeforePull = async ({
27242
27538
  for (const [relativePath, entry] of Object.entries(manifest.files)) {
27243
27539
  manifestPathByRemoteFileId.set(entry.remoteFileId, relativePath);
27244
27540
  }
27245
- for (const { entry, relativePath } of files) {
27541
+ for (const remoteFile of files) {
27542
+ const { entry, relativePath } = remoteFile;
27246
27543
  if (!entry.id) {
27247
27544
  continue;
27248
27545
  }
@@ -27250,18 +27547,40 @@ var reconcileRemoteRenamesBeforePull = async ({
27250
27547
  if (!manifestPath || manifestPath === relativePath) {
27251
27548
  continue;
27252
27549
  }
27253
- const sourcePath = path7.join(localPath, manifestPath);
27254
- const destinationPath = path7.join(localPath, relativePath);
27550
+ const sourcePath = path9.join(localPath, manifestPath);
27551
+ const destinationPath = path9.join(localPath, relativePath);
27255
27552
  const manifestEntry = manifest.files[manifestPath];
27256
27553
  const destinationExists = await pathExists(destinationPath);
27257
27554
  const localChanged = await hasTrackedLocalChange({
27258
27555
  localPath,
27259
27556
  manifestEntry,
27260
27557
  relativePath: manifestPath
27261
- });
27558
+ }) || Boolean(conflictPolicy) && !await pathExists(sourcePath);
27559
+ const localMoveCandidates = conflictPolicy && !await pathExists(sourcePath) ? (localFiles ?? []).filter(
27560
+ (file2) => !manifest.files[file2.relativePath] && file2.hash === manifestEntry.hash
27561
+ ) : [];
27562
+ if (conflictPolicy === "local" && (localChanged || destinationExists) && client) {
27563
+ const winnerPath = localMoveCandidates.length === 1 ? localMoveCandidates[0].relativePath : manifestPath;
27564
+ if (!dryRun && relativePath !== winnerPath) {
27565
+ await client.movePathByRef({
27566
+ ...storeRef,
27567
+ sourcePath: toStorePath2(relativePath),
27568
+ destinationPath: toStorePath2(winnerPath)
27569
+ });
27570
+ }
27571
+ manifest.files[winnerPath] = {
27572
+ ...manifestEntry,
27573
+ remotePath: toStorePath2(winnerPath)
27574
+ };
27575
+ if (winnerPath !== manifestPath) delete manifest.files[manifestPath];
27576
+ remoteFile.conflictWinner = "local";
27577
+ remoteFile.relativePath = winnerPath;
27578
+ remoteFile.entry = { ...entry, path: toStorePath2(winnerPath) };
27579
+ continue;
27580
+ }
27262
27581
  const plan = planRemoteRenameBeforePull({
27263
27582
  destinationExists,
27264
- force,
27583
+ force: force || conflictPolicy === "remote",
27265
27584
  localChanged
27266
27585
  });
27267
27586
  if (plan.type === "conflict") {
@@ -27288,6 +27607,33 @@ var reconcileRemoteRenamesBeforePull = async ({
27288
27607
  );
27289
27608
  continue;
27290
27609
  }
27610
+ if (conflictPolicy === "remote" && (localChanged || destinationExists)) {
27611
+ remoteFile.conflictWinner = "remote";
27612
+ for (const candidate of localMoveCandidates) {
27613
+ if (!dryRun)
27614
+ await deleteLocalFileIfPresent(
27615
+ path9.join(localPath, candidate.relativePath)
27616
+ );
27617
+ }
27618
+ }
27619
+ if (conflictPolicy && localFiles) {
27620
+ const source = localFiles.find(
27621
+ (file2) => file2.relativePath === manifestPath
27622
+ );
27623
+ const remaining = localFiles.filter(
27624
+ (file2) => file2.relativePath !== manifestPath && file2.relativePath !== relativePath && !localMoveCandidates.includes(file2)
27625
+ );
27626
+ if (source && !localChanged && !destinationExists)
27627
+ remaining.push({ ...source, relativePath });
27628
+ localFiles.splice(0, localFiles.length, ...remaining);
27629
+ if (dryRun) {
27630
+ manifest.files[relativePath] = {
27631
+ ...manifestEntry,
27632
+ remotePath: toStorePath2(relativePath)
27633
+ };
27634
+ delete manifest.files[manifestPath];
27635
+ }
27636
+ }
27291
27637
  if (!dryRun) {
27292
27638
  if (plan.shouldDeleteDestination) {
27293
27639
  await deleteLocalFileIfPresent(destinationPath);
@@ -27330,6 +27676,9 @@ var localRenameCandidateDestinations = (relativePath, manifest, localFilesByHash
27330
27676
  var localRenameCandidateSources = (file2, deletePaths, manifest) => deletePaths.filter((relativePath) => manifest.files[relativePath]?.hash === file2.hash).sort();
27331
27677
  var preflightLocalRenamesBeforePush = async ({
27332
27678
  client,
27679
+ conflictPolicy,
27680
+ remoteFiles,
27681
+ localPath,
27333
27682
  deletePaths,
27334
27683
  dryRun,
27335
27684
  localFiles,
@@ -27413,6 +27762,41 @@ var preflightLocalRenamesBeforePush = async ({
27413
27762
  if (!manifestEntry) {
27414
27763
  continue;
27415
27764
  }
27765
+ const remoteFile = remoteFiles?.find(
27766
+ (file2) => file2.entry.id === manifestEntry.remoteFileId
27767
+ );
27768
+ if (conflictPolicy && remoteFile && localPath && remoteFile.entry.revisionNumber !== manifestEntry.remoteRevisionNumber) {
27769
+ remoteFile.conflictWinner = conflictPolicy;
27770
+ if (conflictPolicy === "local") {
27771
+ if (!dryRun)
27772
+ await client.movePathByRef({
27773
+ ...storeRef,
27774
+ sourcePath: toStorePath2(rename5.sourcePath),
27775
+ destinationPath: toStorePath2(rename5.destinationPath)
27776
+ });
27777
+ remoteFile.relativePath = rename5.destinationPath;
27778
+ remoteFile.entry = {
27779
+ ...remoteFile.entry,
27780
+ path: toStorePath2(rename5.destinationPath)
27781
+ };
27782
+ manifest.files[rename5.destinationPath] = {
27783
+ ...manifestEntry,
27784
+ remotePath: toStorePath2(rename5.destinationPath)
27785
+ };
27786
+ delete manifest.files[rename5.sourcePath];
27787
+ } else {
27788
+ if (!dryRun)
27789
+ await moveLocalFileIfPresent(
27790
+ path9.join(localPath, rename5.destinationPath),
27791
+ path9.join(localPath, rename5.sourcePath)
27792
+ );
27793
+ const movedFile = localFiles.find(
27794
+ (file2) => file2.relativePath === rename5.destinationPath
27795
+ );
27796
+ if (movedFile) movedFile.relativePath = rename5.sourcePath;
27797
+ }
27798
+ continue;
27799
+ }
27416
27800
  if (!dryRun) {
27417
27801
  await client.movePathByRef({
27418
27802
  ...storeRef,
@@ -27439,14 +27823,10 @@ var preflightLocalRenamesBeforePush = async ({
27439
27823
  remainingDeletePaths
27440
27824
  };
27441
27825
  };
27442
- var localIgnoreFilePath = (rootDir) => path7.join(
27443
- rootDir,
27444
- resolveCheckoutStateDirectoryName(rootDir),
27445
- localIgnoreFilename
27446
- );
27826
+ var localIgnoreFilePath = (rootDir) => path9.join(rootDir, resolveCheckoutStateDirectoryName(), localIgnoreFilename);
27447
27827
  var readLocalIgnorePatterns = async (rootDir) => {
27448
27828
  try {
27449
- const raw = await readFile2(localIgnoreFilePath(rootDir), "utf8");
27829
+ const raw = await readFile3(localIgnoreFilePath(rootDir), "utf8");
27450
27830
  return parseLocalIgnoreFile(raw);
27451
27831
  } catch (error51) {
27452
27832
  if (errorCode(error51) === "ENOENT") {
@@ -27455,20 +27835,6 @@ var readLocalIgnorePatterns = async (rootDir) => {
27455
27835
  throw error51;
27456
27836
  }
27457
27837
  };
27458
- var readStoreIgnoreCache = async (rootDir) => {
27459
- const manifest = await readCheckoutFilesManifest(rootDir);
27460
- return manifest.ignorePatterns;
27461
- };
27462
- var writeStoreIgnoreCache = async (rootDir, storeIgnorePatterns) => {
27463
- const manifest = await readCheckoutFilesManifest(rootDir);
27464
- const ignorePatterns = resolveIgnorePatterns({
27465
- bakedIn: [],
27466
- storeSetting: storeIgnorePatterns,
27467
- localOverride: []
27468
- });
27469
- await writeCheckoutFilesManifest(rootDir, { ...manifest, ignorePatterns });
27470
- return ignorePatterns;
27471
- };
27472
27838
  var readIgnoreOnboardedAt = async (rootDir) => {
27473
27839
  const manifest = await readCheckoutFilesManifest(rootDir);
27474
27840
  return manifest.ignoreOnboardedAt;
@@ -27484,22 +27850,29 @@ var resolveCheckoutConfig = async (startDir = process.cwd()) => {
27484
27850
  const config2 = await findForumProjectConfig(startDir);
27485
27851
  if (!config2) {
27486
27852
  throw new Error(
27487
- "No forum.yaml found. Run fm init <store> or fm clone <store> first."
27853
+ "No .forum/sync.yaml found. Run fm init <store> or fm clone <store> first."
27488
27854
  );
27489
27855
  }
27490
- const rootDir = path7.dirname(config2.path);
27491
- const [cachedStoreIgnore, localIgnore] = await Promise.all([
27492
- readStoreIgnoreCache(rootDir),
27493
- readLocalIgnorePatterns(rootDir)
27494
- ]);
27495
- const ignore3 = resolveIgnorePatterns({
27496
- bakedIn: [],
27497
- storeSetting: cachedStoreIgnore,
27498
- localOverride: localIgnore
27499
- });
27856
+ const rootDir = path9.dirname(path9.dirname(config2.path));
27857
+ const personal = await readLocalIgnorePatterns(rootDir);
27858
+ const ignorePatternsByStore = {};
27859
+ const ignoreMatchersByRule = {};
27860
+ for (const rule of config2.syncRules) {
27861
+ const shared = await readSharedIgnoreFile(sharedIgnorePath(rootDir, rule.store)) ?? [];
27862
+ ignorePatternsByStore[rule.store] = [...shared, ...personal];
27863
+ ignoreMatchersByRule[rule.id] = createCheckoutIgnoreMatcher(
27864
+ rule,
27865
+ shared,
27866
+ personal
27867
+ );
27868
+ }
27869
+ const stores = Object.keys(ignorePatternsByStore);
27870
+ const ignore3 = stores.length === 1 ? ignorePatternsByStore[stores[0]] : personal;
27500
27871
  return {
27501
27872
  ...config2,
27873
+ ignoreMatchersByRule,
27502
27874
  ignore: ignore3,
27875
+ ignorePatternsByStore,
27503
27876
  configRootDir: rootDir,
27504
27877
  rootDir
27505
27878
  };
@@ -27507,8 +27880,8 @@ var resolveCheckoutConfig = async (startDir = process.cwd()) => {
27507
27880
  var collectLocalCheckoutFiles = async (rootDir, ignorePatterns, syncIncludePaths) => {
27508
27881
  return (await scanLocalCheckoutFiles(rootDir, ignorePatterns, syncIncludePaths)).files;
27509
27882
  };
27510
- var scanLocalCheckoutFiles = async (rootDir, ignorePatterns, syncIncludePaths, progress) => {
27511
- const matcher = createIgnoreMatcher(ignorePatterns);
27883
+ var scanLocalCheckoutFiles = async (rootDir, ignorePatterns, syncIncludePaths, progress, ignoreMatcher) => {
27884
+ const matcher = ignoreMatcher ?? createIgnoreMatcher(ignorePatterns);
27512
27885
  const includePaths = normalizeCheckoutIncludePaths(syncIncludePaths);
27513
27886
  const scan = {
27514
27887
  files: [],
@@ -27530,18 +27903,76 @@ var scanLocalCheckoutFiles = async (rootDir, ignorePatterns, syncIncludePaths, p
27530
27903
  );
27531
27904
  return scan;
27532
27905
  };
27906
+ var ignoreSettingDetail = (change) => {
27907
+ const message = change.action === "conflict" ? `Shared ignore rules changed locally and remotely for ${change.store}. Save your local rules, remove ${change.relativePath}, and sync to restore the store baseline. Then apply your merged rules and sync again.` : `Shared ignore settings: ${change.action} (${change.store})`;
27908
+ if (change.action === "conflict")
27909
+ return checkoutConflictFileResult({
27910
+ relativePath: change.relativePath,
27911
+ message,
27912
+ type: "shared-ignore-conflict"
27913
+ });
27914
+ return checkoutFileResultWithDirection(
27915
+ {
27916
+ relativePath: change.relativePath,
27917
+ message,
27918
+ status: "succeeded",
27919
+ changeType: "updated"
27920
+ },
27921
+ change.action === "publish" ? "upload" : "download"
27922
+ );
27923
+ };
27533
27924
  var pullCheckout = async ({
27534
27925
  client,
27535
27926
  dryRun = false,
27536
27927
  filePaths,
27537
27928
  force = false,
27538
27929
  ignorePatterns,
27930
+ ignoreMatcher,
27931
+ ignoreMatchersByRule,
27932
+ ignoreSettings,
27933
+ ignorePatternsByStore,
27539
27934
  localPath,
27540
27935
  progress,
27541
27936
  storeRef,
27542
27937
  syncIncludePaths,
27543
27938
  syncRules
27544
27939
  }) => {
27940
+ if (syncRules && ignoreSettings) {
27941
+ const settings = await prepareCheckoutIgnore({
27942
+ rootDir: localPath,
27943
+ syncRules,
27944
+ client,
27945
+ dryRun: dryRun || ignoreSettings === "preview"
27946
+ });
27947
+ const conflicts = settings.changes.filter(
27948
+ (change) => change.action === "conflict"
27949
+ );
27950
+ if (conflicts.length)
27951
+ return checkoutResultFromDetails(conflicts.map(ignoreSettingDetail));
27952
+ const result = await pullCheckout({
27953
+ client,
27954
+ dryRun,
27955
+ filePaths,
27956
+ force,
27957
+ ignorePatterns,
27958
+ localPath,
27959
+ progress,
27960
+ storeRef,
27961
+ syncIncludePaths,
27962
+ syncRules,
27963
+ ignorePatternsByStore: settings.patternsByStore,
27964
+ ignoreMatchersByRule: settings.matchersByRule
27965
+ });
27966
+ const changes = dryRun ? settings.changes.filter((change) => change.action !== "publish") : [];
27967
+ Object.assign(
27968
+ result,
27969
+ checkoutResultFromDetails([
27970
+ ...result.details,
27971
+ ...changes.map(ignoreSettingDetail)
27972
+ ])
27973
+ );
27974
+ return result;
27975
+ }
27545
27976
  const requestedRelativePaths = normalizeCheckoutFilePaths(filePaths);
27546
27977
  if (syncRules) {
27547
27978
  await ensureManifestSyncRules(localPath, syncRules, {
@@ -27582,7 +28013,8 @@ var pullCheckout = async ({
27582
28013
  dryRun,
27583
28014
  filePaths: ruleFilePaths ?? void 0,
27584
28015
  force,
27585
- ignorePatterns,
28016
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
28017
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
27586
28018
  localPath,
27587
28019
  progress: checkoutProgressWithSyncRuleRemoteRefs(
27588
28020
  progress,
@@ -27613,7 +28045,7 @@ var pullCheckout = async ({
27613
28045
  );
27614
28046
  }
27615
28047
  const remoteEntries = [];
27616
- const matcher = createIgnoreMatcher(ignorePatterns);
28048
+ const matcher = ignoreMatcher ?? createIgnoreMatcher(ignorePatterns);
27617
28049
  const includePaths = normalizeCheckoutIncludePaths(syncIncludePaths);
27618
28050
  const manifest = await readCheckoutFilesManifest(localPath);
27619
28051
  const manifestSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -27662,6 +28094,7 @@ var pullCheckout = async ({
27662
28094
  const files = remoteEntries.filter(
27663
28095
  (file2) => isCheckoutPathIncluded2(file2.relativePath, false, includePaths)
27664
28096
  );
28097
+ const markdownLinkTargets = markdownLinkTargetsFromEntries(files);
27665
28098
  const remotePaths = new Set(files.map((file2) => file2.relativePath));
27666
28099
  const remoteRenameConflictDetails = dryRun ? [] : await reconcileRemoteRenamesBeforePull({
27667
28100
  dryRun,
@@ -27723,7 +28156,7 @@ var pullCheckout = async ({
27723
28156
  }
27724
28157
  if (!dryRun) {
27725
28158
  const deletedLocalFile = await deleteLocalFileIfPresent(
27726
- path7.join(localPath, relativePath)
28159
+ path9.join(localPath, relativePath)
27727
28160
  );
27728
28161
  delete manifest.files[relativePath];
27729
28162
  manifest.references[relativePath] = {
@@ -27808,7 +28241,7 @@ var pullCheckout = async ({
27808
28241
  );
27809
28242
  }
27810
28243
  const bytes = new Uint8Array(await response.arrayBuffer());
27811
- await atomicWriteBinaryFile(path7.join(localPath, relativePath), bytes);
28244
+ await atomicWriteBinaryFile(path9.join(localPath, relativePath), bytes);
27812
28245
  updateFileManifestEntry({
27813
28246
  storageClass: "binary",
27814
28247
  hash: hashBytes(bytes),
@@ -27827,14 +28260,20 @@ var pullCheckout = async ({
27827
28260
  if (!remoteFile?.content || !("text" in remoteFile.content)) {
27828
28261
  throw new Error("remote file did not include text content");
27829
28262
  }
28263
+ const localText = rewriteMarkdownForLocalCheckout({
28264
+ client,
28265
+ relativePath,
28266
+ targets: markdownLinkTargets,
28267
+ text: remoteFile.content.text
28268
+ });
27830
28269
  await atomicWriteTextFile(
27831
- path7.join(localPath, relativePath),
27832
- remoteFile.content.text,
28270
+ path9.join(localPath, relativePath),
28271
+ localText,
27833
28272
  remoteFile.isExecutable === true
27834
28273
  );
27835
28274
  updateFileManifestEntry({
27836
28275
  storageClass: "text",
27837
- hash: hashText(remoteFile.content.text),
28276
+ hash: hashText(localText),
27838
28277
  manifest,
27839
28278
  path: relativePath,
27840
28279
  remoteFile,
@@ -27980,6 +28419,7 @@ var pullStoreFiles = async ({
27980
28419
  }
27981
28420
  const downloadProgress = withCheckoutProgressDirection(progress, "download");
27982
28421
  downloadProgress?.onTotal?.(files.length + missingRemoteDetails.length);
28422
+ const markdownLinkTargets = markdownLinkTargetsFromEntries(files);
27983
28423
  return runInParallel(
27984
28424
  files,
27985
28425
  (file2) => file2.relativePath,
@@ -27997,7 +28437,7 @@ var pullStoreFiles = async ({
27997
28437
  if (!remoteFile) {
27998
28438
  throw new Error("remote file disappeared while pulling");
27999
28439
  }
28000
- const destinationPath = path7.join(localPath, targetPath, relativePath);
28440
+ const destinationPath = path9.join(localPath, targetPath, relativePath);
28001
28441
  const changeType = await localPathExists(destinationPath) ? "updated" : "created";
28002
28442
  if (remoteFileHasBlobContent(remoteFile)) {
28003
28443
  const { downloadUrl } = await client.createFileDownloadUrlByRef({
@@ -28017,9 +28457,15 @@ var pullStoreFiles = async ({
28017
28457
  if (!remoteFile.content || !("text" in remoteFile.content)) {
28018
28458
  throw new Error("remote file did not include text content");
28019
28459
  }
28460
+ const localText = rewriteMarkdownForLocalCheckout({
28461
+ client,
28462
+ relativePath,
28463
+ targets: markdownLinkTargets,
28464
+ text: remoteFile.content.text
28465
+ });
28020
28466
  await atomicWriteTextFile(
28021
28467
  destinationPath,
28022
- remoteFile.content.text,
28468
+ localText,
28023
28469
  remoteFile.isExecutable === true
28024
28470
  );
28025
28471
  return { changeType, status: "succeeded" };
@@ -28042,12 +28488,52 @@ var pushCheckout = async ({
28042
28488
  filePaths,
28043
28489
  force,
28044
28490
  ignorePatterns,
28491
+ ignoreMatcher,
28492
+ ignoreMatchersByRule,
28493
+ ignoreSettings,
28494
+ ignorePatternsByStore,
28045
28495
  localPath,
28046
28496
  progress,
28047
28497
  storeRef,
28048
28498
  syncIncludePaths,
28049
28499
  syncRules
28050
28500
  }) => {
28501
+ if (syncRules && ignoreSettings) {
28502
+ const settings = await prepareCheckoutIgnore({
28503
+ rootDir: localPath,
28504
+ syncRules,
28505
+ client,
28506
+ dryRun: dryRun || ignoreSettings === "preview"
28507
+ });
28508
+ const conflicts = settings.changes.filter(
28509
+ (change) => change.action === "conflict"
28510
+ );
28511
+ if (conflicts.length)
28512
+ return checkoutResultFromDetails(conflicts.map(ignoreSettingDetail));
28513
+ const result2 = await pushCheckout({
28514
+ client,
28515
+ dryRun,
28516
+ filePaths,
28517
+ force,
28518
+ ignorePatterns,
28519
+ localPath,
28520
+ progress,
28521
+ storeRef,
28522
+ syncIncludePaths,
28523
+ syncRules,
28524
+ ignorePatternsByStore: settings.patternsByStore,
28525
+ ignoreMatchersByRule: settings.matchersByRule
28526
+ });
28527
+ const changes = dryRun ? settings.changes.filter((change) => change.action === "publish") : [];
28528
+ Object.assign(
28529
+ result2,
28530
+ checkoutResultFromDetails([
28531
+ ...result2.details,
28532
+ ...changes.map(ignoreSettingDetail)
28533
+ ])
28534
+ );
28535
+ return result2;
28536
+ }
28051
28537
  const requestedRelativePaths = normalizeCheckoutFilePaths(filePaths);
28052
28538
  if (syncRules) {
28053
28539
  await ensureManifestSyncRules(localPath, syncRules, {
@@ -28088,7 +28574,8 @@ var pushCheckout = async ({
28088
28574
  dryRun,
28089
28575
  filePaths: ruleFilePaths ?? void 0,
28090
28576
  force,
28091
- ignorePatterns,
28577
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
28578
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
28092
28579
  localPath,
28093
28580
  progress: checkoutProgressWithSyncRuleRemoteRefs(
28094
28581
  progress,
@@ -28118,14 +28605,20 @@ var pushCheckout = async ({
28118
28605
  "upload"
28119
28606
  );
28120
28607
  }
28121
- const matcher = createIgnoreMatcher(ignorePatterns);
28608
+ const matcher = ignoreMatcher ?? createIgnoreMatcher(ignorePatterns);
28122
28609
  const includePaths = normalizeCheckoutIncludePaths(syncIncludePaths);
28123
28610
  const scan = requestedRelativePaths ? await scanTargetLocalCheckoutFiles(
28124
28611
  localPath,
28125
28612
  requestedRelativePaths,
28126
28613
  includePaths,
28127
28614
  matcher
28128
- ) : await scanLocalCheckoutFiles(localPath, ignorePatterns, includePaths);
28615
+ ) : await scanLocalCheckoutFiles(
28616
+ localPath,
28617
+ ignorePatterns,
28618
+ includePaths,
28619
+ void 0,
28620
+ matcher
28621
+ );
28129
28622
  const manifest = await readCheckoutFilesManifest(localPath);
28130
28623
  const manifestSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
28131
28624
  const localPaths = new Set(scan.files.map((file2) => file2.relativePath));
@@ -28273,7 +28766,11 @@ var pushCheckout = async ({
28273
28766
  relativePath: file2.relativePath,
28274
28767
  remoteRevisionNumber: manifestEntry?.remoteRevisionNumber,
28275
28768
  storeRef,
28276
- text: file2.text
28769
+ text: rewriteMarkdownForRemoteStore({
28770
+ manifest,
28771
+ relativePath: file2.relativePath,
28772
+ text: file2.text
28773
+ })
28277
28774
  });
28278
28775
  const pushed = await client.writeTextFileByRef(writeInput);
28279
28776
  updateFileManifestEntry({
@@ -28381,7 +28878,7 @@ var pullRemoteFileIntoCheckout = async ({
28381
28878
  );
28382
28879
  }
28383
28880
  const bytes = new Uint8Array(await response.arrayBuffer());
28384
- await atomicWriteBinaryFile(path7.join(localPath, relativePath), bytes);
28881
+ await atomicWriteBinaryFile(path9.join(localPath, relativePath), bytes);
28385
28882
  updateFileManifestEntry({
28386
28883
  storageClass: "binary",
28387
28884
  hash: hashBytes(bytes),
@@ -28400,14 +28897,20 @@ var pullRemoteFileIntoCheckout = async ({
28400
28897
  if (!remoteFile.content || !("text" in remoteFile.content)) {
28401
28898
  throw new Error("remote file did not include text content");
28402
28899
  }
28900
+ const localText = rewriteMarkdownForLocalCheckout({
28901
+ client,
28902
+ relativePath,
28903
+ targets: markdownLinkTargetsFromManifest(manifest),
28904
+ text: remoteFile.content.text
28905
+ });
28403
28906
  await atomicWriteTextFile(
28404
- path7.join(localPath, relativePath),
28405
- remoteFile.content.text,
28907
+ path9.join(localPath, relativePath),
28908
+ localText,
28406
28909
  remoteFile.isExecutable === true
28407
28910
  );
28408
28911
  updateFileManifestEntry({
28409
28912
  storageClass: "text",
28410
- hash: hashText(remoteFile.content.text),
28913
+ hash: hashText(localText),
28411
28914
  manifest,
28412
28915
  path: relativePath,
28413
28916
  remoteFile,
@@ -28448,7 +28951,11 @@ var pushLocalFileFromCheckout = async ({
28448
28951
  relativePath: file2.relativePath,
28449
28952
  remoteRevisionNumber: manifestEntry?.remoteRevisionNumber,
28450
28953
  storeRef,
28451
- text: file2.text
28954
+ text: rewriteMarkdownForRemoteStore({
28955
+ manifest,
28956
+ relativePath: file2.relativePath,
28957
+ text: file2.text
28958
+ })
28452
28959
  })
28453
28960
  );
28454
28961
  updateFileManifestEntry({
@@ -28490,7 +28997,7 @@ var remoteFileHash = async ({
28490
28997
  client,
28491
28998
  remoteFile,
28492
28999
  storeRef,
28493
- path: path20
29000
+ path: path22
28494
29001
  }) => {
28495
29002
  const textHash = remoteTextHash(remoteFile);
28496
29003
  if (textHash) {
@@ -28501,7 +29008,7 @@ var remoteFileHash = async ({
28501
29008
  }
28502
29009
  const { downloadUrl } = await client.createFileDownloadUrlByRef({
28503
29010
  ...storeRef,
28504
- path: path20
29011
+ path: path22
28505
29012
  });
28506
29013
  const response = await fetch(downloadUrl);
28507
29014
  if (!response.ok) {
@@ -28651,6 +29158,7 @@ var deleteRemoteFileFromCheckout = async ({
28651
29158
  };
28652
29159
  var executeSyncCheckoutFiles = async ({
28653
29160
  client,
29161
+ conflictPolicy,
28654
29162
  dryRun,
28655
29163
  force,
28656
29164
  localPath,
@@ -28675,6 +29183,7 @@ var executeSyncCheckoutFiles = async ({
28675
29183
  paths,
28676
29184
  (relativePath) => relativePath,
28677
29185
  async (relativePath) => syncCheckoutFile({
29186
+ conflictPolicy,
28678
29187
  client,
28679
29188
  dryRun,
28680
29189
  force,
@@ -28701,6 +29210,7 @@ var ignoredCheckoutFileResult = (ignored) => ({
28701
29210
  });
28702
29211
  var planSyncCheckoutSession = async ({
28703
29212
  client,
29213
+ conflictPolicy,
28704
29214
  dryRun,
28705
29215
  files,
28706
29216
  force,
@@ -28717,6 +29227,10 @@ var planSyncCheckoutSession = async ({
28717
29227
  remoteFileCount: files.length
28718
29228
  });
28719
29229
  const remoteRenameConflictDetails = await reconcileRemoteRenamesBeforePull({
29230
+ client,
29231
+ conflictPolicy,
29232
+ localFiles: scan.files,
29233
+ storeRef,
28720
29234
  dryRun,
28721
29235
  files,
28722
29236
  force,
@@ -28726,7 +29240,7 @@ var planSyncCheckoutSession = async ({
28726
29240
  const remoteRenameConflictPaths = checkoutDetailPathSet(
28727
29241
  remoteRenameConflictDetails
28728
29242
  );
28729
- const snapshot = buildSyncCheckoutSnapshot({
29243
+ let snapshot = buildSyncCheckoutSnapshot({
28730
29244
  includePaths,
28731
29245
  isIgnored: (relativePath) => isIgnored(relativePath, false, includePaths, matcher),
28732
29246
  localFiles: scan.files,
@@ -28742,6 +29256,9 @@ var planSyncCheckoutSession = async ({
28742
29256
  phase: "local-rename-preflight"
28743
29257
  });
28744
29258
  const structuralPreflight = await preflightLocalRenamesBeforePush({
29259
+ conflictPolicy,
29260
+ remoteFiles: files,
29261
+ localPath,
28745
29262
  client,
28746
29263
  deletePaths: snapshot.deletePaths,
28747
29264
  dryRun,
@@ -28749,6 +29266,26 @@ var planSyncCheckoutSession = async ({
28749
29266
  manifest,
28750
29267
  storeRef
28751
29268
  });
29269
+ if (conflictPolicy) {
29270
+ snapshot = buildSyncCheckoutSnapshot({
29271
+ includePaths,
29272
+ isIgnored: (relativePath) => isIgnored(relativePath, false, includePaths, matcher),
29273
+ localFiles: scan.files,
29274
+ manifestFiles: Object.keys(manifest.files).map((relativePath) => ({
29275
+ relativePath
29276
+ })),
29277
+ remoteBlockedPaths: remoteRenameConflictPaths,
29278
+ remoteFiles: files
29279
+ });
29280
+ }
29281
+ const policyDetails = await resolveAmbiguousSyncMoves({
29282
+ conflictPolicy,
29283
+ dryRun,
29284
+ localPath,
29285
+ manifestFiles: manifest.files,
29286
+ snapshot,
29287
+ structuralPreflight
29288
+ });
28752
29289
  const preflightDetails = [
28753
29290
  ...scan.ignored.map(ignoredCheckoutFileResult),
28754
29291
  ...scan.unsupported.map(unsupportedCheckoutFileResult)
@@ -28763,6 +29300,7 @@ var planSyncCheckoutSession = async ({
28763
29300
  });
28764
29301
  return {
28765
29302
  explicitDetails: [
29303
+ ...withCheckoutDetailsDirection(policyDetails, "download"),
28766
29304
  ...withCheckoutDetailsDirection(remoteRenameConflictDetails, "download"),
28767
29305
  ...withCheckoutDetailsDirection(
28768
29306
  structuralPreflight.movedDetails,
@@ -28798,6 +29336,7 @@ var readSyncCheckoutRemoteFiles = async ({
28798
29336
  };
28799
29337
  var scanSyncCheckoutLocalFiles = async ({
28800
29338
  ignorePatterns,
29339
+ ignoreMatcher,
28801
29340
  includePaths,
28802
29341
  localPath,
28803
29342
  progress
@@ -28806,7 +29345,8 @@ var scanSyncCheckoutLocalFiles = async ({
28806
29345
  localPath,
28807
29346
  ignorePatterns,
28808
29347
  includePaths,
28809
- progress
29348
+ progress,
29349
+ ignoreMatcher
28810
29350
  );
28811
29351
  reportCheckoutProgressEvent(
28812
29352
  progress,
@@ -28860,6 +29400,7 @@ var finalizeSyncCheckoutSession = async ({
28860
29400
  };
28861
29401
  var syncCheckoutFile = async ({
28862
29402
  client,
29403
+ conflictPolicy,
28863
29404
  dryRun,
28864
29405
  force,
28865
29406
  isInsideRemoteReference,
@@ -28874,7 +29415,7 @@ var syncCheckoutFile = async ({
28874
29415
  const manifestEntry = manifest.files[relativePath];
28875
29416
  const remoteKind = remoteFile ? isReferenceEntry(remoteFile.entry) ? "reference" : "file" : "missing";
28876
29417
  let remoteVersionChanged = false;
28877
- if (remoteKind === "file" && remoteFile && localFile && manifestEntry) {
29418
+ if (remoteKind === "file" && remoteFile && manifestEntry) {
28878
29419
  if (typeof remoteFile.entry.revisionNumber === "number") {
28879
29420
  remoteVersionChanged = (manifestEntry.remoteRevisionNumber ?? null) !== remoteFile.entry.revisionNumber;
28880
29421
  } else {
@@ -28888,18 +29429,19 @@ var syncCheckoutFile = async ({
28888
29429
  remoteVersionChanged = (manifestEntry.remoteRevisionNumber ?? null) !== remoteRevisionNumberForConflictCheck(remote);
28889
29430
  }
28890
29431
  }
28891
- if (isReferenceManifestEntry(manifestEntry)) {
29432
+ if (isReferenceManifestEntry(manifestEntry) && (!conflictPolicy || remoteKind === "missing")) {
28892
29433
  const localHash = localFile?.hash ?? null;
28893
29434
  const localChanged = localHash === null || localHash !== manifestEntry.hash;
28894
29435
  if (remoteKind === "missing") {
28895
- if (dryRun) {
29436
+ if (dryRun && !conflictPolicy) {
28896
29437
  return withCheckoutWorkerDirection(
28897
29438
  localChanged ? "deleted" : "ignored",
28898
29439
  "download"
28899
29440
  );
28900
29441
  }
28901
29442
  const result = await reconcileTrackedRemoteDelete({
28902
- force,
29443
+ dryRun,
29444
+ force: force || conflictPolicy === "remote",
28903
29445
  localFileHash: localHash,
28904
29446
  localPath,
28905
29447
  manifest,
@@ -28916,7 +29458,7 @@ var syncCheckoutFile = async ({
28916
29458
  return withCheckoutWorkerDirection(
28917
29459
  await deleteRemoteFileFromCheckout({
28918
29460
  client,
28919
- force,
29461
+ force: force || conflictPolicy === "local",
28920
29462
  manifest,
28921
29463
  manifestEntry,
28922
29464
  relativePath,
@@ -28946,7 +29488,7 @@ var syncCheckoutFile = async ({
28946
29488
  await pushLocalFileFromCheckout({
28947
29489
  client,
28948
29490
  file: localFile,
28949
- force,
29491
+ force: force || conflictPolicy === "local",
28950
29492
  manifest,
28951
29493
  manifestEntry,
28952
29494
  manifestSyncedAt,
@@ -29029,6 +29571,8 @@ var syncCheckoutFile = async ({
29029
29571
  }
29030
29572
  }
29031
29573
  const plan = planSyncCheckoutFile({
29574
+ resolvedConflict: remoteFile?.conflictWinner,
29575
+ conflictPolicy,
29032
29576
  force,
29033
29577
  localIsExecutable: localFile?.storageClass === "text" ? localFile.isExecutable : false,
29034
29578
  localHash: localFile?.hash ?? null,
@@ -29072,7 +29616,7 @@ var syncCheckoutFile = async ({
29072
29616
  if (!remoteFile || !isReferenceEntry(remoteFile.entry)) {
29073
29617
  throw new Error("planned reference accept without remote reference");
29074
29618
  }
29075
- const deletedLocalFile = !dryRun && (localFile || manifestEntry) ? await deleteLocalFileIfPresent(path7.join(localPath, relativePath)) : false;
29619
+ const deletedLocalFile = !dryRun && (localFile || manifestEntry) ? await deleteLocalFileIfPresent(path9.join(localPath, relativePath)) : false;
29076
29620
  if (!dryRun) {
29077
29621
  delete manifest.files[relativePath];
29078
29622
  manifest.references[relativePath] = {
@@ -29091,7 +29635,7 @@ var syncCheckoutFile = async ({
29091
29635
  }
29092
29636
  const result = await reconcileTrackedRemoteDelete({
29093
29637
  dryRun,
29094
- force,
29638
+ force: force || conflictPolicy === "remote",
29095
29639
  localFileHash: localFile?.hash ?? null,
29096
29640
  localPath,
29097
29641
  manifest,
@@ -29101,7 +29645,11 @@ var syncCheckoutFile = async ({
29101
29645
  return typeof result === "string" || result.status !== "failed" ? withCheckoutWorkerDirection(result, "download") : result;
29102
29646
  }
29103
29647
  if (plan.type === "pullRemote") {
29104
- if (!remoteFile) {
29648
+ const pullEntry = remoteFile?.entry ?? (remotePathExists ? await client.readFileByRef({
29649
+ ...storeRef,
29650
+ path: toStorePath2(relativePath)
29651
+ }) : null);
29652
+ if (!pullEntry) {
29105
29653
  throw new Error("planned pull without remote file");
29106
29654
  }
29107
29655
  if (dryRun) {
@@ -29116,13 +29664,13 @@ var syncCheckoutFile = async ({
29116
29664
  return withCheckoutWorkerDirection(
29117
29665
  await pullRemoteFileIntoCheckout({
29118
29666
  client,
29119
- entry: remoteFile.entry,
29667
+ entry: pullEntry,
29120
29668
  localPath,
29121
29669
  manifest,
29122
29670
  manifestEntry,
29123
29671
  manifestSyncedAt,
29124
29672
  relativePath,
29125
- source: remoteFile.source,
29673
+ source: remoteFile?.source,
29126
29674
  storeRef
29127
29675
  }),
29128
29676
  "download"
@@ -29141,13 +29689,17 @@ var syncCheckoutFile = async ({
29141
29689
  "upload"
29142
29690
  );
29143
29691
  }
29692
+ const pushManifestEntry = conflictPolicy === "local" && manifestEntry?.storageClass === "binary" && remoteFile ? {
29693
+ ...manifestEntry,
29694
+ remoteRevisionNumber: remoteFile.entry.revisionNumber
29695
+ } : manifestEntry;
29144
29696
  return withCheckoutWorkerDirection(
29145
29697
  await pushLocalFileFromCheckout({
29146
29698
  client,
29147
29699
  file: localFile,
29148
- force,
29700
+ force: force || conflictPolicy === "local",
29149
29701
  manifest,
29150
- manifestEntry,
29702
+ manifestEntry: pushManifestEntry,
29151
29703
  manifestSyncedAt,
29152
29704
  storeRef
29153
29705
  }),
@@ -29164,7 +29716,7 @@ var syncCheckoutFile = async ({
29164
29716
  return withCheckoutWorkerDirection(
29165
29717
  await deleteRemoteFileFromCheckout({
29166
29718
  client,
29167
- force,
29719
+ force: force || conflictPolicy === "local",
29168
29720
  manifest,
29169
29721
  manifestEntry,
29170
29722
  relativePath,
@@ -29177,16 +29729,18 @@ var syncCheckoutFile = async ({
29177
29729
  };
29178
29730
  var syncCheckoutRuleFiles = async ({
29179
29731
  client,
29732
+ conflictPolicy,
29180
29733
  dryRun,
29181
29734
  force,
29182
29735
  ignorePatterns,
29736
+ ignoreMatcher,
29183
29737
  localPath,
29184
29738
  progress,
29185
29739
  referenceSyncMode,
29186
29740
  storeRef,
29187
29741
  syncIncludePaths
29188
29742
  }) => {
29189
- const matcher = createIgnoreMatcher(ignorePatterns);
29743
+ const matcher = ignoreMatcher ?? createIgnoreMatcher(ignorePatterns);
29190
29744
  const includePaths = normalizeCheckoutIncludePaths(syncIncludePaths);
29191
29745
  const manifest = await readCheckoutFilesManifest(localPath);
29192
29746
  const manifestSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -29197,12 +29751,14 @@ var syncCheckoutRuleFiles = async ({
29197
29751
  storeRef
29198
29752
  });
29199
29753
  const scan = await scanSyncCheckoutLocalFiles({
29754
+ ignoreMatcher,
29200
29755
  ignorePatterns,
29201
29756
  includePaths,
29202
29757
  localPath,
29203
29758
  progress
29204
29759
  });
29205
29760
  const sessionPlan = await planSyncCheckoutSession({
29761
+ conflictPolicy,
29206
29762
  client,
29207
29763
  dryRun,
29208
29764
  files,
@@ -29224,6 +29780,7 @@ var syncCheckoutRuleFiles = async ({
29224
29780
  progress?.onTotal?.(explicitDetails.length + paths.length);
29225
29781
  reportCheckoutDetails(progress, explicitDetails);
29226
29782
  const result = await executeSyncCheckoutFiles({
29783
+ conflictPolicy,
29227
29784
  client,
29228
29785
  dryRun,
29229
29786
  force,
@@ -29255,9 +29812,14 @@ var syncCheckoutRuleFiles = async ({
29255
29812
  var syncCheckout = async ({
29256
29813
  beforeRuleApply,
29257
29814
  client,
29815
+ conflictPolicy,
29258
29816
  dryRun = false,
29259
29817
  force,
29260
29818
  ignorePatterns,
29819
+ ignoreMatcher,
29820
+ ignoreMatchersByRule,
29821
+ ignoreSettings,
29822
+ ignorePatternsByStore,
29261
29823
  localPath,
29262
29824
  progress,
29263
29825
  referenceSyncMode = false,
@@ -29265,6 +29827,44 @@ var syncCheckout = async ({
29265
29827
  syncIncludePaths,
29266
29828
  syncRules
29267
29829
  }) => {
29830
+ if (syncRules && ignoreSettings) {
29831
+ const settings = await prepareCheckoutIgnore({
29832
+ rootDir: localPath,
29833
+ syncRules,
29834
+ client,
29835
+ dryRun: dryRun || ignoreSettings === "preview"
29836
+ });
29837
+ const conflicts = settings.changes.filter(
29838
+ (change) => change.action === "conflict"
29839
+ );
29840
+ if (conflicts.length)
29841
+ return checkoutResultFromDetails(conflicts.map(ignoreSettingDetail));
29842
+ const result = await syncCheckout({
29843
+ beforeRuleApply,
29844
+ client,
29845
+ conflictPolicy,
29846
+ dryRun,
29847
+ force,
29848
+ ignorePatterns,
29849
+ localPath,
29850
+ progress,
29851
+ referenceSyncMode,
29852
+ storeRef,
29853
+ syncIncludePaths,
29854
+ syncRules,
29855
+ ignorePatternsByStore: settings.patternsByStore,
29856
+ ignoreMatchersByRule: settings.matchersByRule
29857
+ });
29858
+ const changes = settings.changes;
29859
+ Object.assign(
29860
+ result,
29861
+ checkoutResultFromDetails([
29862
+ ...result.details,
29863
+ ...changes.map(ignoreSettingDetail)
29864
+ ])
29865
+ );
29866
+ return result;
29867
+ }
29268
29868
  if (syncRules) {
29269
29869
  reportCheckoutProgressEvent(progress, "Checking checkout sync rules", {
29270
29870
  phase: "sync-rules",
@@ -29364,15 +29964,18 @@ var syncCheckout = async ({
29364
29964
  client: ruleClient,
29365
29965
  dryRun: true,
29366
29966
  force,
29367
- ignorePatterns,
29967
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
29968
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
29368
29969
  localPath,
29369
29970
  storeRef: ruleStoreRef,
29370
29971
  syncIncludePaths: ruleIncludePaths
29371
29972
  }) : await syncCheckoutRuleFiles({
29973
+ conflictPolicy,
29372
29974
  client: ruleClient,
29373
29975
  dryRun: true,
29374
29976
  force,
29375
- ignorePatterns,
29977
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
29978
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
29376
29979
  localPath,
29377
29980
  referenceSyncMode: isStoreReferenceRule,
29378
29981
  storeRef: ruleStoreRef,
@@ -29396,16 +29999,19 @@ var syncCheckout = async ({
29396
29999
  client: ruleClient,
29397
30000
  dryRun,
29398
30001
  force,
29399
- ignorePatterns,
30002
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
30003
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
29400
30004
  localPath,
29401
30005
  progress: ruleProgress,
29402
30006
  storeRef: ruleStoreRef,
29403
30007
  syncIncludePaths: ruleIncludePaths
29404
30008
  }) : await syncCheckoutRuleFiles({
30009
+ conflictPolicy,
29405
30010
  client: ruleClient,
29406
30011
  dryRun,
29407
30012
  force,
29408
- ignorePatterns,
30013
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
30014
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
29409
30015
  localPath,
29410
30016
  progress: ruleProgress,
29411
30017
  referenceSyncMode: isStoreReferenceRule,
@@ -29460,6 +30066,8 @@ var syncCheckout = async ({
29460
30066
  );
29461
30067
  }
29462
30068
  return syncCheckoutRuleFiles({
30069
+ ignoreMatcher,
30070
+ conflictPolicy,
29463
30071
  client,
29464
30072
  dryRun,
29465
30073
  force,
@@ -29472,16 +30080,16 @@ var syncCheckout = async ({
29472
30080
  });
29473
30081
  };
29474
30082
  var ensureCheckoutDirectoryReady = async (directoryPath) => {
29475
- await mkdir3(directoryPath, { recursive: true });
30083
+ await mkdir4(directoryPath, { recursive: true });
29476
30084
  const entries = await readdir2(directoryPath);
29477
30085
  if (entries.length > 0) {
29478
30086
  throw new Error(`Target directory must be empty: ${directoryPath}`);
29479
30087
  }
29480
30088
  };
29481
30089
  var ensureProjectConfigDoesNotExist = async (directoryPath) => {
29482
- for (const filename of reservedRootFileNames) {
30090
+ for (const filename of projectConfigFilenames) {
29483
30091
  try {
29484
- const filePath = path7.join(directoryPath, filename);
30092
+ const filePath = path9.join(directoryPath, filename);
29485
30093
  await stat(filePath);
29486
30094
  throw new Error(`Forum config already exists: ${filePath}`);
29487
30095
  } catch (error51) {
@@ -29494,142 +30102,40 @@ var ensureProjectConfigDoesNotExist = async (directoryPath) => {
29494
30102
  };
29495
30103
 
29496
30104
  // src/ignoreCommand.ts
29497
- import ignore2 from "ignore";
29498
- var toRelative = (storePath) => storePath.replace(/^\/+/, "");
29499
- var sortPaths = (paths) => [...paths].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
29500
- var planIgnoreAdd = (pattern, currentPatterns, remoteFilePaths) => {
29501
- const trimmed = pattern.trim();
29502
- if (!trimmed) {
29503
- throw new Error("Ignore pattern cannot be empty.");
29504
- }
29505
- const alreadyPresent = currentPatterns.includes(trimmed);
29506
- const nextPatterns = resolveIgnorePatterns({
29507
- bakedIn: [],
29508
- storeSetting: currentPatterns,
29509
- localOverride: [trimmed]
29510
- });
29511
- const matcher = ignore2().add([trimmed]);
29512
- const matchedRemotePaths = sortPaths(
29513
- remoteFilePaths.filter((storePath) => {
29514
- const relative = toRelative(storePath);
29515
- return Boolean(relative) && matcher.ignores(relative);
29516
- })
29517
- );
29518
- return { pattern: trimmed, alreadyPresent, nextPatterns, matchedRemotePaths };
29519
- };
29520
- var collectRemoteFilePaths = async (client, storeRef) => {
29521
- const listing = await client.listTreeByRef({
29522
- ...storeRef,
29523
- directoryPath: "/",
29524
- includeHidden: true
29525
- });
29526
- const entries = listing?.entries ?? [];
29527
- return sortPaths(
29528
- entries.filter((entry) => entry.type !== "directory").map((entry) => entry.path)
29529
- );
29530
- };
29531
- var executeIgnoreAdd = async (client, storeRef, pattern, currentPatterns, io) => {
29532
- const remoteFilePaths = await collectRemoteFilePaths(client, storeRef);
29533
- const plan = planIgnoreAdd(pattern, currentPatterns, remoteFilePaths);
29534
- let settingUpdated = false;
29535
- if (!plan.alreadyPresent) {
29536
- await client.upsertStoreByRef({
29537
- ...storeRef,
29538
- patch: { ignore: plan.nextPatterns }
29539
- });
29540
- settingUpdated = true;
29541
- }
29542
- const deletedRemotePaths = [];
29543
- let deletedRemoteDirectories = [];
29544
- if (plan.matchedRemotePaths.length > 0 && await io.confirmRemoteDelete(plan)) {
29545
- for (const storePath of plan.matchedRemotePaths) {
29546
- await client.deleteFileByRef({ ...storeRef, path: storePath });
29547
- deletedRemotePaths.push(storePath);
29548
- }
29549
- deletedRemoteDirectories = await cleanupRemoteCheckoutDirectories({
29550
- client,
29551
- removedPaths: deletedRemotePaths.map(toRelative),
29552
- storeRef
29553
- });
29554
- }
30105
+ var planIgnoreAdd = (pattern, currentPatterns) => {
30106
+ if (!pattern.trim()) throw new Error("Ignore pattern cannot be empty.");
30107
+ const alreadyPresent = currentPatterns.at(-1) === pattern;
29555
30108
  return {
29556
- plan,
29557
- settingUpdated,
29558
- deletedRemotePaths,
29559
- deletedRemoteDirectories
30109
+ pattern,
30110
+ alreadyPresent,
30111
+ nextPatterns: alreadyPresent ? currentPatterns : [...currentPatterns, pattern]
29560
30112
  };
29561
30113
  };
29562
30114
  var removeIgnorePattern = (pattern, currentPatterns) => {
29563
- const trimmed = pattern.trim();
29564
- if (!trimmed) {
29565
- throw new Error("Ignore pattern cannot be empty.");
29566
- }
29567
- const nextPatterns = currentPatterns.filter((current) => current !== trimmed);
30115
+ if (!pattern.trim()) throw new Error("Ignore pattern cannot be empty.");
30116
+ const nextPatterns = currentPatterns.filter((current) => current !== pattern);
29568
30117
  return {
29569
30118
  nextPatterns,
29570
30119
  removed: nextPatterns.length !== currentPatterns.length,
29571
- pattern: trimmed
30120
+ pattern
29572
30121
  };
29573
30122
  };
29574
- var setIgnorePatterns = (patterns) => resolveIgnorePatterns({
29575
- bakedIn: [],
29576
- storeSetting: [],
29577
- localOverride: patterns
29578
- });
30123
+ var setIgnorePatterns = (patterns) => {
30124
+ if (patterns.some((pattern) => !pattern.trim()))
30125
+ throw new Error("Ignore pattern cannot be empty.");
30126
+ return patterns;
30127
+ };
29579
30128
 
29580
30129
  // src/runIgnoreCommand.ts
29581
30130
  var runAdd = async (deps) => {
29582
- const {
29583
- client,
29584
- storeRef,
29585
- currentPatterns,
29586
- pattern,
29587
- assumeYes,
29588
- confirmYesNo: confirmYesNo2,
29589
- renderOutput: renderOutput2,
29590
- commandNames: commandNames2
29591
- } = deps;
29592
- if (!pattern) {
29593
- throw new Error(`Usage: ${commandNames2} ignore add <pattern>`);
29594
- }
29595
- const io = {
29596
- confirmRemoteDelete: (plan) => confirmYesNo2({
29597
- assumeYes,
29598
- nonInteractiveMessage: "Refusing to delete store files without confirmation. Re-run with --yes.",
29599
- prompt: `Delete ${plan.matchedRemotePaths.length} matching file(s) from the store? [y/N] `
29600
- })
29601
- };
29602
- const result = await executeIgnoreAdd(
29603
- client,
29604
- storeRef,
29605
- pattern,
29606
- currentPatterns,
29607
- io
29608
- );
29609
- if (result.settingUpdated) {
29610
- renderOutput2(`Added ignore rule: ${result.plan.pattern}`);
29611
- } else {
29612
- renderOutput2(`Ignore rule already present: ${result.plan.pattern}`);
29613
- }
29614
- if (result.plan.matchedRemotePaths.length === 0) {
29615
- renderOutput2("No already-pushed files matched.");
29616
- } else if (result.deletedRemotePaths.length > 0) {
29617
- renderOutput2(
29618
- `Deleted ${result.deletedRemotePaths.length} file(s) from the store.`
29619
- );
29620
- } else {
29621
- renderOutput2("Left already-pushed matches in place.");
29622
- }
29623
- if (result.deletedRemoteDirectories.length > 0) {
29624
- renderOutput2(
29625
- `Deleted ${result.deletedRemoteDirectories.length} empty director${result.deletedRemoteDirectories.length === 1 ? "y" : "ies"} from the store.`
29626
- );
29627
- }
30131
+ const { currentPatterns, pattern, renderOutput: renderOutput2, commandNames: commandNames2 } = deps;
30132
+ if (!pattern) throw new Error(`Usage: ${commandNames2} ignore add <pattern>`);
30133
+ const plan = planIgnoreAdd(pattern, currentPatterns);
30134
+ if (!plan.alreadyPresent) await deps.writePatterns(plan.nextPatterns);
30135
+ renderOutput2(`Added ignore rule: ${plan.pattern}. Run fm sync to publish.`);
29628
30136
  };
29629
30137
  var runList = async (deps) => {
29630
- const { client, storeRef, renderOutput: renderOutput2 } = deps;
29631
- const metadata = await client.readStoreByRef(storeRef);
29632
- const ignore3 = metadata?.ignore ?? [];
30138
+ const { currentPatterns: ignore3, renderOutput: renderOutput2 } = deps;
29633
30139
  if (ignore3.length === 0) {
29634
30140
  renderOutput2("(no ignore rules)");
29635
30141
  return;
@@ -29640,8 +30146,7 @@ var runList = async (deps) => {
29640
30146
  };
29641
30147
  var runRemove = async (deps) => {
29642
30148
  const {
29643
- client,
29644
- storeRef,
30149
+ writePatterns,
29645
30150
  currentPatterns,
29646
30151
  pattern,
29647
30152
  renderOutput: renderOutput2,
@@ -29655,14 +30160,11 @@ var runRemove = async (deps) => {
29655
30160
  renderOutput2(`Ignore rule not present: ${result.pattern}`);
29656
30161
  return;
29657
30162
  }
29658
- await client.upsertStoreByRef({
29659
- ...storeRef,
29660
- patch: { ignore: result.nextPatterns }
29661
- });
30163
+ await writePatterns(result.nextPatterns);
29662
30164
  renderOutput2(`Removed ignore rule: ${result.pattern}`);
29663
30165
  };
29664
30166
  var runSet = async (deps) => {
29665
- const { client, storeRef, patterns, renderOutput: renderOutput2, commandNames: commandNames2 } = deps;
30167
+ const { writePatterns, patterns, renderOutput: renderOutput2, commandNames: commandNames2 } = deps;
29666
30168
  if (patterns.length === 0) {
29667
30169
  throw new Error(`Usage: ${commandNames2} ignore set <pattern>...`);
29668
30170
  }
@@ -29670,24 +30172,17 @@ var runSet = async (deps) => {
29670
30172
  if (nextPatterns.length === 0) {
29671
30173
  throw new Error("Ignore pattern cannot be empty.");
29672
30174
  }
29673
- await client.upsertStoreByRef({
29674
- ...storeRef,
29675
- patch: { ignore: nextPatterns }
29676
- });
30175
+ await writePatterns(nextPatterns);
29677
30176
  renderOutput2(`Set ${nextPatterns.length} ignore rule(s).`);
29678
30177
  };
29679
30178
  var runClear = async (deps) => {
29680
- const { client, storeRef, renderOutput: renderOutput2 } = deps;
29681
- await client.upsertStoreByRef({
29682
- ...storeRef,
29683
- patch: { ignore: [] }
29684
- });
30179
+ const { writePatterns, renderOutput: renderOutput2 } = deps;
30180
+ await writePatterns([]);
29685
30181
  renderOutput2("Cleared ignore rules.");
29686
30182
  };
29687
30183
  var runAuto = async (deps) => {
29688
30184
  const {
29689
- client,
29690
- storeRef,
30185
+ writePatterns,
29691
30186
  currentPatterns,
29692
30187
  assumeYes,
29693
30188
  confirmYesNo: confirmYesNo2,
@@ -29696,9 +30191,9 @@ var runAuto = async (deps) => {
29696
30191
  renderOutput: renderOutput2
29697
30192
  } = deps;
29698
30193
  const analysis = await analyzeLocal();
29699
- const additions = analysis.proposedPatterns.filter(
29700
- (proposed) => !currentPatterns.includes(proposed)
29701
- );
30194
+ const proposed = analysis.proposedPatterns;
30195
+ const alreadyAppended = JSON.stringify(currentPatterns.slice(-proposed.length)) === JSON.stringify(proposed);
30196
+ const additions = alreadyAppended ? [] : proposed;
29702
30197
  if (additions.length === 0) {
29703
30198
  renderOutput2("Nothing to suggest \u2014 no new junk patterns detected.");
29704
30199
  await markOnboarded();
@@ -29711,19 +30206,18 @@ var runAuto = async (deps) => {
29711
30206
  const confirmed = await confirmYesNo2({
29712
30207
  assumeYes,
29713
30208
  nonInteractiveMessage: "Refusing to write ignore rules without confirmation. Re-run with --yes.",
29714
- prompt: `Add ${additions.length} ignore rule(s) to the store? [Y/n] `
30209
+ prompt: `Add ${additions.length} ignore rule(s) to the shared ignore file? [Y/n] `
29715
30210
  });
29716
30211
  if (!confirmed) {
29717
- renderOutput2("Left the store setting unchanged.");
30212
+ renderOutput2("Left the shared ignore file unchanged.");
29718
30213
  await markOnboarded();
29719
30214
  return;
29720
30215
  }
29721
- await client.upsertStoreByRef({
29722
- ...storeRef,
29723
- patch: { ignore: [...currentPatterns, ...additions] }
29724
- });
30216
+ await writePatterns([...currentPatterns, ...additions]);
29725
30217
  await markOnboarded();
29726
- renderOutput2(`Added ${additions.length} ignore rule(s) to the store.`);
30218
+ renderOutput2(
30219
+ `Added ${additions.length} ignore rule(s) locally. Run fm sync to publish.`
30220
+ );
29727
30221
  };
29728
30222
  var runIgnoreCommand = async (deps) => {
29729
30223
  const { subcommand, commandNames: commandNames2 } = deps;
@@ -29780,8 +30274,7 @@ var warnImminent = (analysis, renderWarning2) => {
29780
30274
  };
29781
30275
  var maybePromptIgnoreSetup = async (deps) => {
29782
30276
  const {
29783
- client,
29784
- storeRef,
30277
+ writePatterns,
29785
30278
  currentPatterns,
29786
30279
  analysis,
29787
30280
  onboarded,
@@ -29822,13 +30315,10 @@ var maybePromptIgnoreSetup = async (deps) => {
29822
30315
  }
29823
30316
  const action = await chooseAction(analysis);
29824
30317
  if (action === "auto") {
29825
- await client.upsertStoreByRef({
29826
- ...storeRef,
29827
- patch: { ignore: analysis.proposedPatterns }
29828
- });
30318
+ await writePatterns(analysis.proposedPatterns);
29829
30319
  await markOnboarded();
29830
30320
  renderOutput2(
29831
- `Added ${analysis.proposedPatterns.length} ignore rule(s) to the store.`
30321
+ `Added ${analysis.proposedPatterns.length} shared ignore rule(s) locally.`
29832
30322
  );
29833
30323
  return {
29834
30324
  outcome: "auto-applied",
@@ -29855,10 +30345,10 @@ var maybePromptIgnoreSetup = async (deps) => {
29855
30345
 
29856
30346
  // src/collectIgnoreMarkers.ts
29857
30347
  import { stat as stat2 } from "fs/promises";
29858
- import path8 from "path";
30348
+ import path10 from "path";
29859
30349
  var exists = async (rootDir, rule) => {
29860
30350
  try {
29861
- const stats = await stat2(path8.join(rootDir, rule.marker));
30351
+ const stats = await stat2(path10.join(rootDir, rule.marker));
29862
30352
  return rule.markerKind === "dir" ? stats.isDirectory() : stats.isFile();
29863
30353
  } catch {
29864
30354
  return false;
@@ -29877,11 +30367,11 @@ var collectIgnoreMarkers = async (rootDir, rules = IGNORE_PROPOSAL_RULES) => {
29877
30367
  };
29878
30368
 
29879
30369
  // src/readGitignorePatterns.ts
29880
- import { readFile as readFile3 } from "fs/promises";
29881
- import path9 from "path";
30370
+ import { readFile as readFile4 } from "fs/promises";
30371
+ import path11 from "path";
29882
30372
  var readGitignorePatterns = async (rootDir) => {
29883
30373
  try {
29884
- const raw = await readFile3(path9.join(rootDir, ".gitignore"), "utf8");
30374
+ const raw = await readFile4(path11.join(rootDir, ".gitignore"), "utf8");
29885
30375
  return parseLocalIgnoreFile(raw);
29886
30376
  } catch {
29887
30377
  return [];
@@ -29945,9 +30435,9 @@ var createHumanWatchSubscriber = ({
29945
30435
  return;
29946
30436
  }
29947
30437
  case "sync.watch.conflict.raised": {
29948
- const path20 = String(details.path ?? "");
30438
+ const path22 = String(details.path ?? "");
29949
30439
  const suggestions = details.suggestions ?? [];
29950
- error51(`Conflict: ${path20} \u2014 ${event.message ?? "needs attention"}`);
30440
+ error51(`Conflict: ${path22} \u2014 ${event.message ?? "needs attention"}`);
29951
30441
  if (suggestions.length > 0) {
29952
30442
  error51(` ${formatSuggestions(suggestions)}`);
29953
30443
  }
@@ -29976,10 +30466,10 @@ import notifier from "node-notifier";
29976
30466
  var createNotifyWatchSubscriber = () => {
29977
30467
  return (event) => {
29978
30468
  if (event.eventType === "sync.watch.conflict.raised") {
29979
- const path20 = event.details?.path;
30469
+ const path22 = event.details?.path;
29980
30470
  notifier.notify({
29981
30471
  title: "Forum sync conflict",
29982
- message: typeof path20 === "string" ? `${path20} needs attention` : event.message ?? "A file needs attention"
30472
+ message: typeof path22 === "string" ? `${path22} needs attention` : event.message ?? "A file needs attention"
29983
30473
  });
29984
30474
  return;
29985
30475
  }
@@ -30095,10 +30585,10 @@ var createConflictTracker = () => {
30095
30585
  );
30096
30586
  const raised = current.filter((conflict) => !active.has(conflict.path));
30097
30587
  const cleared = [...active.keys()].filter(
30098
- (path20) => !currentByPath.has(path20)
30588
+ (path22) => !currentByPath.has(path22)
30099
30589
  );
30100
- for (const path20 of cleared) {
30101
- active.delete(path20);
30590
+ for (const path22 of cleared) {
30591
+ active.delete(path22);
30102
30592
  }
30103
30593
  for (const conflict of current) {
30104
30594
  active.set(conflict.path, conflict);
@@ -30109,13 +30599,13 @@ var createConflictTracker = () => {
30109
30599
  };
30110
30600
 
30111
30601
  // src/watch/conflictResolutionHints.ts
30112
- var keepLocal = (path20) => ({
30602
+ var keepLocal = (path22) => ({
30113
30603
  label: "Keep local",
30114
- command: `fm push --force ${path20}`
30604
+ command: `fm push --force ${path22}`
30115
30605
  });
30116
- var acceptRemote = (path20) => ({
30606
+ var acceptRemote = (path22) => ({
30117
30607
  label: "Accept remote",
30118
- command: `fm pull --force ${path20}`
30608
+ command: `fm pull --force ${path22}`
30119
30609
  });
30120
30610
  var review = (label, command) => ({
30121
30611
  label,
@@ -30131,23 +30621,23 @@ var forceResolvableTypes = /* @__PURE__ */ new Set([
30131
30621
  "reference-local-file",
30132
30622
  "read-only-local-change"
30133
30623
  ]);
30134
- var conflictResolutionSuggestions = (conflictType, path20) => {
30624
+ var conflictResolutionSuggestions = (conflictType, path22) => {
30135
30625
  if (forceResolvableTypes.has(conflictType)) {
30136
- return [keepLocal(path20), acceptRemote(path20)];
30626
+ return [keepLocal(path22), acceptRemote(path22)];
30137
30627
  }
30138
- return [review("Review", `fm status ${path20}`)];
30628
+ return [review("Review", `fm status ${path22}`)];
30139
30629
  };
30140
30630
 
30141
30631
  // src/watch/extractWatchConflicts.ts
30142
30632
  var toWatchConflict = (issue2) => {
30143
- const { path: path20, message, relatedPaths } = issue2.details.details;
30633
+ const { path: path22, message, relatedPaths } = issue2.details.details;
30144
30634
  const conflictType = issue2.details.type;
30145
30635
  return {
30146
- path: path20,
30636
+ path: path22,
30147
30637
  conflictType,
30148
30638
  message,
30149
30639
  relatedPaths: relatedPaths ?? [],
30150
- suggestions: conflictResolutionSuggestions(conflictType, path20)
30640
+ suggestions: conflictResolutionSuggestions(conflictType, path22)
30151
30641
  };
30152
30642
  };
30153
30643
  var extractWatchConflicts = (result) => {
@@ -30238,9 +30728,9 @@ var createCheckoutWatcher = ({
30238
30728
  })
30239
30729
  );
30240
30730
  }
30241
- for (const path20 of cleared) {
30731
+ for (const path22 of cleared) {
30242
30732
  bus.emit(
30243
- watchEvent("sync.watch.conflict.cleared", void 0, { path: path20 })
30733
+ watchEvent("sync.watch.conflict.cleared", void 0, { path: path22 })
30244
30734
  );
30245
30735
  }
30246
30736
  bus.emit(
@@ -30458,7 +30948,7 @@ var runSyncWatchDirectory = async (options2) => {
30458
30948
  };
30459
30949
 
30460
30950
  // src/watch/fleetStatusSubscriber.ts
30461
- import path10 from "path";
30951
+ import path12 from "path";
30462
30952
  var STATUS_BADGE = {
30463
30953
  starting: "\u2026",
30464
30954
  clean: "\u2713",
@@ -30476,15 +30966,15 @@ var STATUS_COLOR = {
30476
30966
  var deriveLabels = (rootDirs) => {
30477
30967
  const counts = /* @__PURE__ */ new Map();
30478
30968
  for (const dir of rootDirs) {
30479
- const base = path10.basename(dir);
30969
+ const base = path12.basename(dir);
30480
30970
  counts.set(base, (counts.get(base) ?? 0) + 1);
30481
30971
  }
30482
30972
  const labels = /* @__PURE__ */ new Map();
30483
30973
  for (const dir of rootDirs) {
30484
- const base = path10.basename(dir);
30974
+ const base = path12.basename(dir);
30485
30975
  labels.set(
30486
30976
  dir,
30487
- (counts.get(base) ?? 0) > 1 ? path10.join(path10.basename(path10.dirname(dir)), base) : base
30977
+ (counts.get(base) ?? 0) > 1 ? path12.join(path12.basename(path12.dirname(dir)), base) : base
30488
30978
  );
30489
30979
  }
30490
30980
  return labels;
@@ -30583,7 +31073,7 @@ var createFleetStatusSubscriber = (rootDirs, { write, isTty }) => {
30583
31073
  };
30584
31074
 
30585
31075
  // src/watch/discoverCheckouts.ts
30586
- import path11 from "path";
31076
+ import path13 from "path";
30587
31077
  import { readdir as readdir3, stat as stat3 } from "fs/promises";
30588
31078
  var pathExists2 = async (candidate) => {
30589
31079
  try {
@@ -30596,7 +31086,7 @@ var pathExists2 = async (candidate) => {
30596
31086
  var isCheckoutDir = async (dir) => {
30597
31087
  let hasStateDirectory = false;
30598
31088
  for (const name of checkoutStateDirectoryNames) {
30599
- if (await pathExists2(path11.join(dir, name))) {
31089
+ if (await pathExists2(path13.join(dir, name))) {
30600
31090
  hasStateDirectory = true;
30601
31091
  break;
30602
31092
  }
@@ -30605,7 +31095,7 @@ var isCheckoutDir = async (dir) => {
30605
31095
  return false;
30606
31096
  }
30607
31097
  for (const filename of projectConfigFilenames) {
30608
- if (await pathExists2(path11.join(dir, filename))) {
31098
+ if (await pathExists2(path13.join(dir, filename))) {
30609
31099
  return true;
30610
31100
  }
30611
31101
  }
@@ -30620,10 +31110,10 @@ var readSubdirectories = async (dir) => {
30620
31110
  }
30621
31111
  return entries.filter(
30622
31112
  (entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules"
30623
- ).map((entry) => path11.join(dir, entry.name));
31113
+ ).map((entry) => path13.join(dir, entry.name));
30624
31114
  };
30625
31115
  var discoverCheckouts = async (rootPath) => {
30626
- const root = path11.resolve(rootPath);
31116
+ const root = path13.resolve(rootPath);
30627
31117
  const found = [];
30628
31118
  const visit = async (dir) => {
30629
31119
  if (await isCheckoutDir(dir)) {
@@ -30638,51 +31128,27 @@ var discoverCheckouts = async (rootPath) => {
30638
31128
  };
30639
31129
 
30640
31130
  // src/watch/isIgnoredWatchPath.ts
30641
- import path12 from "path";
30642
- var ignoredWatchSegments = /* @__PURE__ */ new Set([
30643
- ".forum",
30644
- ".doctrine",
30645
- ".git",
30646
- "node_modules",
30647
- // Standard build/cache junk — never source content, and the usual offenders
30648
- // for high-churn writes during a build or test run.
30649
- ".cache",
30650
- ".next",
30651
- ".nuxt",
30652
- ".turbo",
30653
- ".svelte-kit",
30654
- "dist",
30655
- "build",
30656
- "coverage",
30657
- "out"
30658
- ]);
30659
- var ignoredRootFiles = /* @__PURE__ */ new Set([
30660
- "forum.yaml",
30661
- "forum.yml",
30662
- "doctrine.yaml",
30663
- "doctrine.yml"
30664
- ]);
31131
+ import path14 from "path";
30665
31132
  var isIgnoredWatchPath = (filename) => {
30666
- if (!filename) {
30667
- return false;
30668
- }
30669
- const relativePath = String(filename).split(path12.sep).join("/");
30670
- const segments2 = relativePath.split("/").filter((segment) => segment !== "");
30671
- if (segments2.some((segment) => ignoredWatchSegments.has(segment))) {
30672
- return true;
30673
- }
30674
- return segments2.length === 1 && ignoredRootFiles.has(segments2[0]);
31133
+ if (!filename) return false;
31134
+ const relativePath = String(filename).split(path14.sep).join("/");
31135
+ const segments2 = relativePath.split("/").filter(Boolean);
31136
+ if (segments2.includes(".git")) return true;
31137
+ const metadataIndex = segments2.indexOf(".forum");
31138
+ if (metadataIndex === -1) return false;
31139
+ const metadataPath = segments2.slice(metadataIndex + 1).join("/");
31140
+ return !(metadataPath === "usr_ignore" || metadataPath === "sync.yaml" || /^stores\/[^/]+\/ignore$/.test(metadataPath));
30675
31141
  };
30676
31142
 
30677
31143
  // src/watch/readPathsFile.ts
30678
- import { readFile as readFile4 } from "fs/promises";
31144
+ import { readFile as readFile5 } from "fs/promises";
30679
31145
  var readPathsFile = async (file2) => {
30680
- const contents = await readFile4(file2, "utf8");
31146
+ const contents = await readFile5(file2, "utf8");
30681
31147
  return contents.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
30682
31148
  };
30683
31149
 
30684
31150
  // src/watch/resolveWatchWorkingSet.ts
30685
- import path13 from "path";
31151
+ import path15 from "path";
30686
31152
  var resolveWatchWorkingSet = async ({ watchPaths, pathsFile }, resolvers) => {
30687
31153
  const explicitPaths = [...watchPaths];
30688
31154
  if (pathsFile !== null) {
@@ -30705,7 +31171,7 @@ var resolveWatchWorkingSet = async ({ watchPaths, pathsFile }, resolvers) => {
30705
31171
  const entries = [];
30706
31172
  const errors = [];
30707
31173
  for (const rawPath of explicitPaths) {
30708
- const resolved = path13.resolve(rawPath);
31174
+ const resolved = path15.resolve(rawPath);
30709
31175
  if (seen.has(resolved)) {
30710
31176
  continue;
30711
31177
  }
@@ -30936,10 +31402,15 @@ var createStoreClient = ({
30936
31402
  { retryDelayMs: transientRetryDelayMs }
30937
31403
  );
30938
31404
  return {
31405
+ createRawFileUrl: (fileId) => `${apiBaseUrl.replace(/\/+$/, "")}/raw-file/${encodeURIComponent(fileId)}`,
30939
31406
  listStores: () => call("stores.list", () => trpc.stores.list.query({})),
30940
31407
  createStore: (input) => call("stores.create", () => trpc.stores.create.mutate(input)),
30941
31408
  deleteStore: (input) => call("stores.delete", () => trpc.stores.delete.mutate(input)),
30942
31409
  readStoreByRef: (input) => call("stores.readByRef", () => trpc.stores.readByRef.query(input)),
31410
+ syncStoreIgnoreByRef: (input) => call(
31411
+ "stores.syncIgnoreByRef",
31412
+ () => trpc.stores.syncIgnoreByRef.mutate(input)
31413
+ ),
30943
31414
  upsertStoreByRef: (input) => call("stores.upsertByRef", () => trpc.stores.upsertByRef.mutate(input)),
30944
31415
  deleteFileByRef: (input) => call(
30945
31416
  "files.deleteFileByRef",
@@ -31071,10 +31542,9 @@ var createClient = (config2) => {
31071
31542
 
31072
31543
  // src/state.ts
31073
31544
  import { homedir } from "os";
31074
- import path14 from "path";
31075
- import { access as access2, mkdir as mkdir4, readFile as readFile5, rename as rename3, writeFile as writeFile4 } from "fs/promises";
31545
+ import path16 from "path";
31546
+ import { mkdir as mkdir5, readFile as readFile6, rename as rename3, writeFile as writeFile4 } from "fs/promises";
31076
31547
  var DEFAULT_STATE_DIRNAME = ".forum";
31077
- var LEGACY_STATE_DIRNAME = ".doctrine";
31078
31548
  var DEFAULT_POLL_INTERVAL_MS = 4e3;
31079
31549
  var DEFAULT_API_BASE_URL = "https://api.forumlabs.net";
31080
31550
  var DEFAULT_CONFIG = {
@@ -31082,49 +31552,32 @@ var DEFAULT_CONFIG = {
31082
31552
  pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
31083
31553
  forceWrites: false
31084
31554
  };
31085
- var legacyStateDir = () => path14.join(homedir(), LEGACY_STATE_DIRNAME);
31086
- var defaultStateDir = () => path14.join(homedir(), DEFAULT_STATE_DIRNAME);
31087
- var configPath = (stateDir) => path14.join(stateDir, "config.json");
31555
+ var defaultStateDir = () => path16.join(homedir(), DEFAULT_STATE_DIRNAME);
31556
+ var configPath = (stateDir) => path16.join(stateDir, "config.json");
31088
31557
  var ensureStateDir = async (stateDir) => {
31089
- await mkdir4(stateDir, { recursive: true });
31558
+ await mkdir5(stateDir, { recursive: true });
31090
31559
  };
31091
31560
  var readJson = async (filePath, fallback) => {
31092
31561
  try {
31093
- const raw = await readFile5(filePath, "utf8");
31562
+ const raw = await readFile6(filePath, "utf8");
31094
31563
  return JSON.parse(raw);
31095
31564
  } catch {
31096
31565
  return fallback;
31097
31566
  }
31098
31567
  };
31099
- var fileExists2 = async (filePath) => {
31100
- try {
31101
- await access2(filePath);
31102
- return true;
31103
- } catch {
31104
- return false;
31105
- }
31106
- };
31107
- var readConfigSourcePath = async (stateDir) => {
31108
- const currentPath = configPath(stateDir);
31109
- if (stateDir !== defaultStateDir() || await fileExists2(currentPath)) {
31110
- return currentPath;
31111
- }
31112
- const legacyPath = configPath(legacyStateDir());
31113
- return await fileExists2(legacyPath) ? legacyPath : currentPath;
31114
- };
31115
31568
  var writeJsonAtomic = async (filePath, value) => {
31116
- const parentDir = path14.dirname(filePath);
31117
- const tempPath = path14.join(
31569
+ const parentDir = path16.dirname(filePath);
31570
+ const tempPath = path16.join(
31118
31571
  parentDir,
31119
- `.${path14.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
31572
+ `.${path16.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
31120
31573
  );
31121
- await mkdir4(parentDir, { recursive: true });
31574
+ await mkdir5(parentDir, { recursive: true });
31122
31575
  await writeFile4(tempPath, JSON.stringify(value, null, 2), "utf8");
31123
31576
  await rename3(tempPath, filePath);
31124
31577
  };
31125
31578
  var readConfig = async (stateDir) => {
31126
31579
  const config2 = await readJson(
31127
- await readConfigSourcePath(stateDir),
31580
+ configPath(stateDir),
31128
31581
  DEFAULT_CONFIG
31129
31582
  );
31130
31583
  return {
@@ -31139,28 +31592,28 @@ var writeConfig = async (stateDir, config2) => {
31139
31592
 
31140
31593
  // src/skillIntegration.ts
31141
31594
  import { constants } from "fs";
31142
- import { access as access3 } from "fs/promises";
31595
+ import { access as access2 } from "fs/promises";
31143
31596
  import os from "os";
31144
- import path17 from "path";
31597
+ import path19 from "path";
31145
31598
  import Enquirer from "enquirer";
31146
31599
 
31147
31600
  // src/skillProviders/claude.ts
31148
- import path15 from "path";
31601
+ import path17 from "path";
31149
31602
  var claudeSkillProvider = {
31150
31603
  id: "claude",
31151
- globalRootCandidates: (home) => [path15.join(home, ".claude", "skills")],
31152
- localRoot: (projectDir) => path15.join(projectDir, ".claude", "skills")
31604
+ globalRootCandidates: (home) => [path17.join(home, ".claude", "skills")],
31605
+ localRoot: (projectDir) => path17.join(projectDir, ".claude", "skills")
31153
31606
  };
31154
31607
 
31155
31608
  // src/skillProviders/codex.ts
31156
- import path16 from "path";
31609
+ import path18 from "path";
31157
31610
  var codexSkillProvider = {
31158
31611
  id: "codex",
31159
31612
  globalRootCandidates: (home) => [
31160
- path16.join(home, ".codex", "skills"),
31161
- path16.join(home, ".agents", "skills")
31613
+ path18.join(home, ".codex", "skills"),
31614
+ path18.join(home, ".agents", "skills")
31162
31615
  ],
31163
- localRoot: (projectDir) => path16.join(projectDir, ".codex", "skills")
31616
+ localRoot: (projectDir) => path18.join(projectDir, ".codex", "skills")
31164
31617
  };
31165
31618
 
31166
31619
  // src/skillProviders/registry.ts
@@ -31181,13 +31634,13 @@ var expandHomePath = (input) => {
31181
31634
  return process.env.HOME || os.homedir();
31182
31635
  }
31183
31636
  if (input.startsWith("~/")) {
31184
- return path17.join(process.env.HOME || os.homedir(), input.slice(2));
31637
+ return path19.join(process.env.HOME || os.homedir(), input.slice(2));
31185
31638
  }
31186
31639
  return input;
31187
31640
  };
31188
31641
  var pathExists3 = async (filePath) => {
31189
31642
  try {
31190
- await access3(filePath);
31643
+ await access2(filePath);
31191
31644
  return true;
31192
31645
  } catch {
31193
31646
  return false;
@@ -31698,33 +32151,84 @@ var editEncryptedText = async ({
31698
32151
 
31699
32152
  // src/openInEditor.ts
31700
32153
  import { spawn } from "child_process";
31701
- import { mkdtemp, readFile as readFile6, rm as rm3, writeFile as writeFile5 } from "fs/promises";
32154
+ import { mkdtemp, readFile as readFile7, rm as rm4, writeFile as writeFile5 } from "fs/promises";
31702
32155
  import { tmpdir } from "os";
31703
- import path18 from "path";
32156
+ import path20 from "path";
31704
32157
  var openInEditor = async (currentText, { extension = "" } = {}) => {
31705
32158
  const editor = process.env.EDITOR?.trim() || "vi";
31706
- const dir = await mkdtemp(path18.join(tmpdir(), "fm-edit-"));
31707
- const tempPath = path18.join(dir, `edit${extension}`);
32159
+ const dir = await mkdtemp(path20.join(tmpdir(), "fm-edit-"));
32160
+ const tempPath = path20.join(dir, `edit${extension}`);
31708
32161
  try {
31709
32162
  await writeFile5(tempPath, currentText, "utf8");
31710
32163
  const exitCode = await new Promise((resolve, reject) => {
31711
- const child = spawn(editor, [tempPath], {
31712
- stdio: "inherit",
31713
- shell: true
31714
- });
32164
+ const child = spawn(
32165
+ "/bin/sh",
32166
+ ["-c", `exec ${editor} "$1"`, "fm-editor", tempPath],
32167
+ { stdio: "inherit" }
32168
+ );
31715
32169
  child.on("error", reject);
31716
- child.on("exit", (code) => resolve(code ?? 0));
32170
+ child.on("exit", (code) => resolve(code ?? 1));
31717
32171
  });
31718
32172
  if (exitCode !== 0) {
31719
32173
  return null;
31720
32174
  }
31721
- return await readFile6(tempPath, "utf8");
32175
+ return await readFile7(tempPath, "utf8");
31722
32176
  } finally {
31723
- await rm3(dir, { force: true, recursive: true }).catch(() => {
32177
+ await rm4(dir, { force: true, recursive: true }).catch(() => {
31724
32178
  });
31725
32179
  }
31726
32180
  };
31727
32181
 
32182
+ // src/readHiddenLine.ts
32183
+ var readHiddenLine = async (prompt) => {
32184
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
32185
+ throw new Error(
32186
+ "Missing encryption password. Provide --password or DOCTRINE_ENCRYPTION_PASSWORD."
32187
+ );
32188
+ }
32189
+ return new Promise((resolve, reject) => {
32190
+ const stdin = process.stdin;
32191
+ const stdout = process.stdout;
32192
+ const wasRaw = stdin.isRaw;
32193
+ let value = "";
32194
+ const cleanup = () => {
32195
+ stdin.off("data", readChunk);
32196
+ stdin.pause();
32197
+ stdin.setRawMode(wasRaw);
32198
+ stdout.write("\n");
32199
+ };
32200
+ const finish = () => {
32201
+ cleanup();
32202
+ resolve(value);
32203
+ };
32204
+ const cancel = () => {
32205
+ cleanup();
32206
+ reject(new Error("Encryption password prompt cancelled."));
32207
+ };
32208
+ const readChunk = (chunk) => {
32209
+ for (const byte of chunk) {
32210
+ if (byte === 3) {
32211
+ cancel();
32212
+ return;
32213
+ }
32214
+ if (byte === 13 || byte === 10) {
32215
+ finish();
32216
+ return;
32217
+ }
32218
+ if (byte === 127) {
32219
+ value = value.slice(0, -1);
32220
+ continue;
32221
+ }
32222
+ value += String.fromCharCode(byte);
32223
+ }
32224
+ };
32225
+ stdin.setRawMode(true);
32226
+ stdin.on("data", readChunk);
32227
+ stdin.resume();
32228
+ stdout.write(prompt);
32229
+ });
32230
+ };
32231
+
31728
32232
  // src/resolveDecryptAuthMode.ts
31729
32233
  var resolveDecryptAuthMode = ({
31730
32234
  encryptionPassword,
@@ -31812,7 +32316,7 @@ Usage:
31812
32316
  fm push [filePath] [--token <jwt>] [--force] [--yes|-y] [--dry-run|-d] [--verbose|--debug]
31813
32317
  fm status [--token <jwt>] [--json]
31814
32318
  fm s [--token <jwt>] [--json]
31815
- fm sync [--token <jwt>] [--force] [--yes|-y] [--verbose] [--watch] [--readonly]
32319
+ fm sync [--token <jwt>] [--conflict|-c local|remote] [--force] [--yes|-y] [--verbose] [--watch] [--readonly]
31816
32320
  fm sync --watch [<path>] [--notify] [--json] [--readonly]
31817
32321
  fm sync --watch <path...> [--paths-file <file>] [--notify] [--json]
31818
32322
  fm sync --dry-run|-d [--token <jwt>] [--json]
@@ -31844,7 +32348,7 @@ Usage:
31844
32348
  fm --version
31845
32349
 
31846
32350
  Store creation:
31847
- fm store create <storeId> creates in defaults.store's workspace from forum.yaml when present, otherwise in the authenticated workspace.
32351
+ fm store create <storeId> creates in defaults.store's workspace from .forum/sync.yaml when present, otherwise in the authenticated workspace.
31848
32352
  fm store create <workspace/storeId> creates in that explicitly selected workspace.
31849
32353
 
31850
32354
  Auth:
@@ -31862,7 +32366,7 @@ Output:
31862
32366
  `;
31863
32367
  var readCliVersion = async () => {
31864
32368
  const packageJson = JSON.parse(
31865
- await readFile7(new URL("../package.json", import.meta.url), "utf8")
32369
+ await readFile8(new URL("../package.json", import.meta.url), "utf8")
31866
32370
  );
31867
32371
  return typeof packageJson.version === "string" ? packageJson.version : "unknown";
31868
32372
  };
@@ -31903,6 +32407,7 @@ var parseArgvCommandName = (argv) => {
31903
32407
  var parseArgs = (argv) => {
31904
32408
  const positional = [];
31905
32409
  const shouldParseDirectPullMapping = argv.includes("pull") && argv.includes("--direct");
32410
+ const isSyncCommand = parseArgvCommandName(argv) === "sync";
31906
32411
  const isCatCommand = parseArgvCommandName(argv) === "cat";
31907
32412
  let token = null;
31908
32413
  const storeFilters = [];
@@ -31922,6 +32427,7 @@ var parseArgs = (argv) => {
31922
32427
  let stateDir = defaultStateDir();
31923
32428
  let pollIntervalMs = null;
31924
32429
  let dryRun = false;
32430
+ let conflictPolicy;
31925
32431
  let force = false;
31926
32432
  let direct = false;
31927
32433
  let json2 = false;
@@ -31968,7 +32474,7 @@ var parseArgs = (argv) => {
31968
32474
  continue;
31969
32475
  }
31970
32476
  if (current === "--file" || current === "-f") {
31971
- inputFilePath = next ? path19.resolve(next) : inputFilePath;
32477
+ inputFilePath = next ? path21.resolve(next) : inputFilePath;
31972
32478
  index += 1;
31973
32479
  continue;
31974
32480
  }
@@ -32033,7 +32539,7 @@ var parseArgs = (argv) => {
32033
32539
  if (!next) {
32034
32540
  throw new Error("--target-dir requires a path.");
32035
32541
  }
32036
- skillTargetDir = path19.resolve(expandHomePath(next));
32542
+ skillTargetDir = path21.resolve(expandHomePath(next));
32037
32543
  index += 1;
32038
32544
  continue;
32039
32545
  }
@@ -32045,7 +32551,7 @@ var parseArgs = (argv) => {
32045
32551
  continue;
32046
32552
  }
32047
32553
  if (current === "--state-dir") {
32048
- stateDir = path19.resolve(next ?? stateDir);
32554
+ stateDir = path21.resolve(next ?? stateDir);
32049
32555
  index += 1;
32050
32556
  continue;
32051
32557
  }
@@ -32053,12 +32559,12 @@ var parseArgs = (argv) => {
32053
32559
  if (!next) {
32054
32560
  throw new Error("--paths-file requires a file path.");
32055
32561
  }
32056
- pathsFile = path19.resolve(expandHomePath(next));
32562
+ pathsFile = path21.resolve(expandHomePath(next));
32057
32563
  index += 1;
32058
32564
  continue;
32059
32565
  }
32060
32566
  if (current.startsWith("--paths-file=")) {
32061
- pathsFile = path19.resolve(
32567
+ pathsFile = path21.resolve(
32062
32568
  expandHomePath(current.slice("--paths-file=".length))
32063
32569
  );
32064
32570
  continue;
@@ -32072,6 +32578,15 @@ var parseArgs = (argv) => {
32072
32578
  openBrowser = false;
32073
32579
  continue;
32074
32580
  }
32581
+ if (isSyncCommand && (current === "-c" || current === "--conflict" || current.startsWith("--conflict="))) {
32582
+ const value = current.startsWith("--conflict=") ? current.slice("--conflict=".length) : next;
32583
+ if (value !== "local" && value !== "remote") {
32584
+ throw new Error("--conflict requires local or remote.");
32585
+ }
32586
+ conflictPolicy = value;
32587
+ if (!current.startsWith("--conflict=")) index += 1;
32588
+ continue;
32589
+ }
32075
32590
  if (current === "--force") {
32076
32591
  force = true;
32077
32592
  continue;
@@ -32124,6 +32639,7 @@ var parseArgs = (argv) => {
32124
32639
  }
32125
32640
  return {
32126
32641
  args: positional,
32642
+ conflictPolicy,
32127
32643
  token,
32128
32644
  storeFilters,
32129
32645
  searchLimit,
@@ -32246,10 +32762,10 @@ var listDirectoryEntries = async (listDirectory, directoryPath) => {
32246
32762
  if (left.type !== "directory" && right.type === "directory") {
32247
32763
  return 1;
32248
32764
  }
32249
- return path19.basename(left.path).localeCompare(path19.basename(right.path));
32765
+ return path21.basename(left.path).localeCompare(path21.basename(right.path));
32250
32766
  }).forEach((entry) => {
32251
32767
  const suffix = entry.type === "directory" ? "/" : "";
32252
- renderOutput(`${path19.basename(entry.path)}${suffix}`);
32768
+ renderOutput(`${path21.basename(entry.path)}${suffix}`);
32253
32769
  });
32254
32770
  };
32255
32771
  var listStores = async (listStoresMetadata) => {
@@ -32544,7 +33060,7 @@ var normalizeSkillAgents = (inputs) => {
32544
33060
  return [...new Set(inputs.map(normalizeSkillAgent))];
32545
33061
  };
32546
33062
  var normalizeRemoteSkillPath = (input) => {
32547
- const normalized = path19.posix.normalize(`/${input.trim()}`);
33063
+ const normalized = path21.posix.normalize(`/${input.trim()}`);
32548
33064
  if (normalized === "/") {
32549
33065
  throw new Error("Skill path must not be the store root.");
32550
33066
  }
@@ -32554,11 +33070,11 @@ var normalizeRemoteSkillPath = (input) => {
32554
33070
  return normalized;
32555
33071
  };
32556
33072
  var parentRemotePath = (filePath) => {
32557
- const parent = path19.posix.dirname(filePath);
33073
+ const parent = path21.posix.dirname(filePath);
32558
33074
  return parent === "." ? "/" : parent;
32559
33075
  };
32560
33076
  var joinRemotePath = (left, right) => {
32561
- return path19.posix.join(left, right).replace(/\/+/g, "/");
33077
+ return path21.posix.join(left, right).replace(/\/+/g, "/");
32562
33078
  };
32563
33079
  var readRemoteSkillTextFile = async (filePath, storeRef, client) => {
32564
33080
  const file2 = await client.readFileByRef({ ...storeRef, path: filePath });
@@ -32620,7 +33136,7 @@ var collectRemoteSkillFiles = async ({
32620
33136
  `Skill contains unsupported ${entry.type} entry: ${entry.path}`
32621
33137
  );
32622
33138
  }
32623
- const relativePath = path19.posix.relative(rootPath, entry.path);
33139
+ const relativePath = path21.posix.relative(rootPath, entry.path);
32624
33140
  if (!relativePath || relativePath.startsWith("..")) {
32625
33141
  throw new Error(
32626
33142
  `Unexpected skill entry outside skill root: ${entry.path}`
@@ -32634,7 +33150,7 @@ var collectRemoteSkillFiles = async ({
32634
33150
  };
32635
33151
  var resolveRemoteSkillRoot = async (inputPath, storeRef, client) => {
32636
33152
  const normalizedPath = normalizeRemoteSkillPath(inputPath);
32637
- if (path19.posix.basename(normalizedPath) === "SKILL.md") {
33153
+ if (path21.posix.basename(normalizedPath) === "SKILL.md") {
32638
33154
  await readRemoteSkillTextFile(normalizedPath, storeRef, client);
32639
33155
  return {
32640
33156
  rootPath: parentRemotePath(normalizedPath),
@@ -32714,7 +33230,7 @@ var resolveSkillInstallTargets = async ({
32714
33230
  return {
32715
33231
  agent,
32716
33232
  rootDir,
32717
- targetDir: path19.join(rootDir, skillName)
33233
+ targetDir: path21.join(rootDir, skillName)
32718
33234
  };
32719
33235
  })
32720
33236
  );
@@ -32730,7 +33246,7 @@ var confirmYesNo = async ({
32730
33246
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
32731
33247
  throw new Error(nonInteractiveMessage);
32732
33248
  }
32733
- const readline = createInterface({
33249
+ const readline = createInterface2({
32734
33250
  input: process.stdin,
32735
33251
  output: process.stdout
32736
33252
  });
@@ -32753,24 +33269,24 @@ var formatSkillTarget = (target, existingTargetDirs) => {
32753
33269
  return ` ${target.agent.padEnd(7)} ${target.targetDir}${suffix}`;
32754
33270
  };
32755
33271
  var writeSkillFiles = async (target, files, replaceExisting) => {
32756
- const temporaryDirectory = path19.join(
33272
+ const temporaryDirectory = path21.join(
32757
33273
  target.rootDir,
32758
- `.${path19.basename(target.targetDir)}.tmp-${process.pid}-${Date.now()}`
33274
+ `.${path21.basename(target.targetDir)}.tmp-${process.pid}-${Date.now()}`
32759
33275
  );
32760
- await rm4(temporaryDirectory, { recursive: true, force: true });
32761
- await mkdir5(temporaryDirectory, { recursive: true });
33276
+ await rm5(temporaryDirectory, { recursive: true, force: true });
33277
+ await mkdir6(temporaryDirectory, { recursive: true });
32762
33278
  try {
32763
33279
  for (const file2 of files) {
32764
- const localPath = path19.join(temporaryDirectory, file2.relativePath);
32765
- await mkdir5(path19.dirname(localPath), { recursive: true });
33280
+ const localPath = path21.join(temporaryDirectory, file2.relativePath);
33281
+ await mkdir6(path21.dirname(localPath), { recursive: true });
32766
33282
  await writeFile6(localPath, file2.bytes);
32767
33283
  }
32768
33284
  if (replaceExisting) {
32769
- await rm4(target.targetDir, { recursive: true, force: true });
33285
+ await rm5(target.targetDir, { recursive: true, force: true });
32770
33286
  }
32771
33287
  await rename4(temporaryDirectory, target.targetDir);
32772
33288
  } catch (error51) {
32773
- await rm4(temporaryDirectory, { recursive: true, force: true });
33289
+ await rm5(temporaryDirectory, { recursive: true, force: true });
32774
33290
  throw error51;
32775
33291
  }
32776
33292
  };
@@ -32813,7 +33329,7 @@ var installSkillBundle = async ({
32813
33329
  throw new Error("Aborted skill install.");
32814
33330
  }
32815
33331
  for (const target of targets) {
32816
- await mkdir5(target.rootDir, { recursive: true });
33332
+ await mkdir6(target.rootDir, { recursive: true });
32817
33333
  }
32818
33334
  for (const target of targets) {
32819
33335
  await writeSkillFiles(
@@ -32845,7 +33361,7 @@ var readRemoteSkillBundle = async ({
32845
33361
  client
32846
33362
  );
32847
33363
  const metadata = parseSkillMetadata(skillMarkdown);
32848
- const rootName = path19.posix.basename(rootPath);
33364
+ const rootName = path21.posix.basename(rootPath);
32849
33365
  if (metadata.name !== rootName) {
32850
33366
  throw new Error(
32851
33367
  `SKILL.md name "${metadata.name}" must match skill directory "${rootName}".`
@@ -33025,7 +33541,7 @@ var readWriteTextInput = async (inlineText, inputFilePath) => {
33025
33541
  throw new Error("Provide either inline text or --file, not both.");
33026
33542
  }
33027
33543
  if (inputFilePath) {
33028
- return readFile7(inputFilePath, "utf8");
33544
+ return readFile8(inputFilePath, "utf8");
33029
33545
  }
33030
33546
  return inlineText ?? null;
33031
33547
  };
@@ -33041,7 +33557,7 @@ var readRemoteTextFile = async (filePath, storeRef, client) => {
33041
33557
  return text;
33042
33558
  };
33043
33559
  var normalizeRemotePath = (input) => {
33044
- const normalized = path19.posix.normalize(`/${input.trim()}`);
33560
+ const normalized = path21.posix.normalize(`/${input.trim()}`);
33045
33561
  if (normalized.includes("\0")) {
33046
33562
  throw new Error("Path contains an invalid null byte.");
33047
33563
  }
@@ -33335,7 +33851,7 @@ var diffRemoteFiles = async ({
33335
33851
  storeRef
33336
33852
  }) => {
33337
33853
  const leftText = await readRemoteTextFile(leftPath, storeRef, client);
33338
- const rightText = localRight ? await readFile7(path19.resolve(rightPath), "utf8") : await readRemoteTextFile(rightPath, storeRef, client);
33854
+ const rightText = localRight ? await readFile8(path21.resolve(rightPath), "utf8") : await readRemoteTextFile(rightPath, storeRef, client);
33339
33855
  return buildUnifiedDiff({
33340
33856
  leftLabel: leftPath,
33341
33857
  leftText,
@@ -33402,8 +33918,8 @@ var copyRemotePath = async (args, storeRef, client) => {
33402
33918
  (entry) => entry.path.startsWith(`${normalizedSource}/`)
33403
33919
  );
33404
33920
  for (const entry of descendants) {
33405
- const relativePath = path19.posix.relative(normalizedSource, entry.path);
33406
- const targetPath = path19.posix.join(normalizedDestination, relativePath);
33921
+ const relativePath = path21.posix.relative(normalizedSource, entry.path);
33922
+ const targetPath = path21.posix.join(normalizedDestination, relativePath);
33407
33923
  if (entry.type === "directory") {
33408
33924
  await createRemoteDirectory(targetPath, storeRef, client);
33409
33925
  continue;
@@ -33533,53 +34049,6 @@ var readCommandTextInput = async (inlineText, inputFilePath) => {
33533
34049
  }
33534
34050
  return null;
33535
34051
  };
33536
- var readHiddenLine = async (prompt) => {
33537
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
33538
- throw new Error(
33539
- "Missing encryption password. Provide --password or DOCTRINE_ENCRYPTION_PASSWORD."
33540
- );
33541
- }
33542
- return new Promise((resolve, reject) => {
33543
- const stdin = process.stdin;
33544
- const stdout = process.stdout;
33545
- const wasRaw = stdin.isRaw;
33546
- let value = "";
33547
- const cleanup = () => {
33548
- stdin.off("data", readChunk);
33549
- stdin.setRawMode(wasRaw);
33550
- stdout.write("\n");
33551
- };
33552
- const finish = () => {
33553
- cleanup();
33554
- resolve(value);
33555
- };
33556
- const cancel = () => {
33557
- cleanup();
33558
- reject(new Error("Encryption password prompt cancelled."));
33559
- };
33560
- const readChunk = (chunk) => {
33561
- for (const byte of chunk) {
33562
- if (byte === 3) {
33563
- cancel();
33564
- return;
33565
- }
33566
- if (byte === 13 || byte === 10) {
33567
- finish();
33568
- return;
33569
- }
33570
- if (byte === 127) {
33571
- value = value.slice(0, -1);
33572
- continue;
33573
- }
33574
- value += String.fromCharCode(byte);
33575
- }
33576
- };
33577
- stdout.write(prompt);
33578
- stdin.setRawMode(true);
33579
- stdin.resume();
33580
- stdin.on("data", readChunk);
33581
- });
33582
- };
33583
34052
  var readEncryptionPassword = async (parsed) => {
33584
34053
  const password = parsed.encryptionPassword ?? process.env.DOCTRINE_ENCRYPTION_PASSWORD;
33585
34054
  if (password) {
@@ -33645,19 +34114,19 @@ var runDecryptCommand = async (rest, parsed, client) => {
33645
34114
  );
33646
34115
  };
33647
34116
  var runEditCommand = async (rest, parsed, client) => {
33648
- const filePath = parsed.inputFilePath ?? (rest[0] ? path19.resolve(rest[0]) : null);
34117
+ const filePath = parsed.inputFilePath ?? (rest[0] ? path21.resolve(rest[0]) : null);
33649
34118
  if (!filePath) {
33650
34119
  throw new Error(
33651
34120
  `Usage: ${commandNames} edit (<localPath>|--file|-f <localPath>) [--password <password>] [--token <jwt>]`
33652
34121
  );
33653
34122
  }
33654
- const encryptedText = await readFile7(filePath, "utf8");
34123
+ const encryptedText = await readFile8(filePath, "utf8");
33655
34124
  const [password, userEncryption] = await Promise.all([
33656
34125
  readEncryptionPassword(parsed),
33657
34126
  readUserEncryption(client)
33658
34127
  ]);
33659
34128
  const result = await editEncryptedText({
33660
- edit: (currentPlaintext) => openInEditor(currentPlaintext, { extension: path19.extname(filePath) }),
34129
+ edit: (currentPlaintext) => openInEditor(currentPlaintext, { extension: path21.extname(filePath) }),
33661
34130
  encryptedText,
33662
34131
  password,
33663
34132
  userEncryption: userEncryption.userEncryption
@@ -33733,10 +34202,10 @@ var runEncryptAddRecipientCommand = async (rest, parsed, client) => {
33733
34202
  `);
33734
34203
  renderOutput(result.encryptedText);
33735
34204
  };
33736
- var defaultEncryptionMigrationBackupPath = () => path19.resolve(process.cwd(), "doctrine-encryption-migration.json");
33737
- var readEncryptionMigrationBackup = async (backupPath) => JSON.parse(await readFile7(backupPath, "utf8"));
34205
+ var defaultEncryptionMigrationBackupPath = () => path21.resolve(process.cwd(), "doctrine-encryption-migration.json");
34206
+ var readEncryptionMigrationBackup = async (backupPath) => JSON.parse(await readFile8(backupPath, "utf8"));
33738
34207
  var writeEncryptionMigrationBackup = async (backupPath, backup) => {
33739
- await mkdir5(path19.dirname(backupPath), { recursive: true });
34208
+ await mkdir6(path21.dirname(backupPath), { recursive: true });
33740
34209
  await writeFile6(backupPath, `${JSON.stringify(backup, null, 2)}
33741
34210
  `, "utf8");
33742
34211
  };
@@ -33753,7 +34222,7 @@ var confirmEncryptionMigration = async ({
33753
34222
  "Refusing to migrate encryption without interactive confirmation. Rerun with --yes to confirm in a non-interactive shell."
33754
34223
  );
33755
34224
  }
33756
- const readline = createInterface({
34225
+ const readline = createInterface2({
33757
34226
  input: process.stdin,
33758
34227
  output: process.stdout
33759
34228
  });
@@ -34024,7 +34493,7 @@ var runEncryptionMigrateCommand = async (rest, parsed, client) => {
34024
34493
  `Usage: ${commandNames} encryption migrate [backup-file] [--store <storeId>] [--password <password>] [--dry-run] [--yes]`
34025
34494
  );
34026
34495
  }
34027
- const backupPath = path19.resolve(
34496
+ const backupPath = path21.resolve(
34028
34497
  rest[1] ?? defaultEncryptionMigrationBackupPath()
34029
34498
  );
34030
34499
  const password = await readEncryptionPassword(parsed);
@@ -34099,7 +34568,7 @@ var doesRemoteQueryMatchPath = (query, filePath) => {
34099
34568
  if (query.startsWith("/")) {
34100
34569
  return normalizedPath === query;
34101
34570
  }
34102
- const filename = path19.basename(filePath).toLowerCase();
34571
+ const filename = path21.basename(filePath).toLowerCase();
34103
34572
  return filename === query || normalizedPath === query || filename.includes(query) || normalizedPath.includes(query);
34104
34573
  };
34105
34574
  var queryRemoteFiles = async (query, storeFilters, client) => {
@@ -34171,7 +34640,7 @@ var printRemoteQueryMatches = (matches) => {
34171
34640
  });
34172
34641
  };
34173
34642
  var normalizeRemoteDirectoryPath = (input) => {
34174
- const normalized = path19.posix.normalize(`/${input.trim()}`);
34643
+ const normalized = path21.posix.normalize(`/${input.trim()}`);
34175
34644
  if (normalized.includes("\0")) {
34176
34645
  throw new Error("Path contains an invalid null byte.");
34177
34646
  }
@@ -34291,12 +34760,12 @@ var labelFromTreeDirectoryPath = (directoryPath) => {
34291
34760
  if (directoryPath === "/") {
34292
34761
  return "./";
34293
34762
  }
34294
- return `${path19.posix.basename(directoryPath)}/`;
34763
+ return `${path21.posix.basename(directoryPath)}/`;
34295
34764
  };
34296
34765
  var doesTreeFilterMatchDirectory = (filter, directoryPath) => {
34297
34766
  const normalizedFilter = filter.trim().toLowerCase();
34298
34767
  const normalizedPath = directoryPath.toLowerCase();
34299
- const basename3 = path19.posix.basename(directoryPath).toLowerCase();
34768
+ const basename3 = path21.posix.basename(directoryPath).toLowerCase();
34300
34769
  return basename3.includes(normalizedFilter) || normalizedPath.includes(normalizedFilter);
34301
34770
  };
34302
34771
  var resolveTreeDirectoryPath = async ({
@@ -34351,7 +34820,7 @@ var printRemoteTreeChildren = async ({
34351
34820
  const childPrefix = isLast ? " " : "\u2502 ";
34352
34821
  const suffix = entry.type === "directory" ? "/" : "";
34353
34822
  lines.push(
34354
- `${prefix}${connector}${path19.posix.basename(entry.path)}${suffix}`
34823
+ `${prefix}${connector}${path21.posix.basename(entry.path)}${suffix}`
34355
34824
  );
34356
34825
  if (entry.type === "directory") {
34357
34826
  await printRemoteTreeChildren({
@@ -34680,12 +35149,6 @@ var unsetConfigValue = async (parsed, key) => {
34680
35149
  });
34681
35150
  renderOutput(`Reset poll-interval-ms to ${DEFAULT_POLL_INTERVAL_MS}`);
34682
35151
  };
34683
- var ensureToken = (token) => {
34684
- if (!token) {
34685
- throw new Error("Missing auth token. Provide --token or DOCTRINE_TOKEN.");
34686
- }
34687
- return token;
34688
- };
34689
35152
  var parseRootStorePath = (storePathInput) => {
34690
35153
  const storeId = normalizeStoreId2(storePathInput);
34691
35154
  return {
@@ -34696,7 +35159,7 @@ var parseRootStorePath = (storePathInput) => {
34696
35159
  var parseNewStorePath = (storePathInput) => {
34697
35160
  return parseRootStorePath(storePathInput);
34698
35161
  };
34699
- var storeCreateSyntaxMessage = "Store ref must be a store ID like 'my-store' or a qualified ref like 'workspace/my-store'. Unqualified store IDs use defaults.store from forum.yaml when present, otherwise the authenticated workspace.";
35162
+ var storeCreateSyntaxMessage = "Store ref must be a store ID like 'my-store' or a qualified ref like 'workspace/my-store'. Unqualified store IDs use defaults.store from .forum/sync.yaml when present, otherwise the authenticated workspace.";
34700
35163
  var parseStoreCreateInput = async (storePathInput) => {
34701
35164
  const segments2 = storePathInput.split("/").map((segment) => segment.trim()).filter(Boolean);
34702
35165
  if (segments2.length === 2) {
@@ -34969,9 +35432,9 @@ var shouldPrintCheckoutDetail = (detail, verbose) => {
34969
35432
  var formatCheckoutDetail = (detail, options2) => {
34970
35433
  const status = checkoutStatusLabel(detail, options2);
34971
35434
  const direction = checkoutDirectionLabel(detail);
34972
- const path20 = formatCheckoutDetailPath(detail, options2.syncRules);
35435
+ const path22 = formatCheckoutDetailPath(detail, options2.syncRules);
34973
35436
  const reason = styleText.dim(checkoutDetailMessage(detail));
34974
- return direction ? `${direction} ${status} ${path20} ${reason}` : `${status} ${path20} ${reason}`;
35437
+ return direction ? `${direction} ${status} ${path22} ${reason}` : `${status} ${path22} ${reason}`;
34975
35438
  };
34976
35439
  var formatCheckoutDetailPath = (detail, syncRules) => {
34977
35440
  const localPath = formatLocalCheckoutPath(detail.relativePath);
@@ -35183,9 +35646,9 @@ var checkoutReportGroups = (details, options2) => {
35183
35646
  );
35184
35647
  };
35185
35648
  var checkoutReportDetailLine = (detail, options2) => {
35186
- const path20 = formatCheckoutDetailPath(detail, options2.syncRules);
35649
+ const path22 = formatCheckoutDetailPath(detail, options2.syncRules);
35187
35650
  const message = detail.status === "failed" || detail.status === "skipped" || detail.status === "ignored" ? detail.message ?? checkoutDetailMessage(detail) : "";
35188
- return message ? ` ${path20} ${styleText.dim(message)}` : ` ${path20}`;
35651
+ return message ? ` ${path22} ${styleText.dim(message)}` : ` ${path22}`;
35189
35652
  };
35190
35653
  var checkoutReportGroupBlock = ({
35191
35654
  dryRun,
@@ -35312,13 +35775,13 @@ var statusChangeKind = (detail) => {
35312
35775
  var formatStatusChange = (detail, syncRules) => {
35313
35776
  const kind = statusChangeKind(detail);
35314
35777
  const tone = kind === "deleted" ? "error" : kind === "added" ? "ok" : "warn";
35315
- const path20 = formatCheckoutDetailPath(detail, syncRules);
35316
- return ` ${styleText.color(tone, kind.padEnd(8))} ${path20}`;
35778
+ const path22 = formatCheckoutDetailPath(detail, syncRules);
35779
+ return ` ${styleText.color(tone, kind.padEnd(8))} ${path22}`;
35317
35780
  };
35318
35781
  var formatStatusBlock = (detail, syncRules) => {
35319
35782
  const message = detail.message ? ` ${styleText.dim(detail.message)}` : "";
35320
- const path20 = formatCheckoutDetailPath(detail, syncRules);
35321
- return ` ${styleText.color("error", "blocked ".padEnd(8))} ${path20}${message}`;
35783
+ const path22 = formatCheckoutDetailPath(detail, syncRules);
35784
+ return ` ${styleText.color("error", "blocked ".padEnd(8))} ${path22}${message}`;
35322
35785
  };
35323
35786
  var actionableStatusDetails = (result) => result.details.filter(
35324
35787
  (detail) => detail.status === "succeeded" || detail.status === "deleted"
@@ -35451,7 +35914,7 @@ var assertCheckoutSyncEnabled = (checkout) => {
35451
35914
  return;
35452
35915
  }
35453
35916
  throw new Error(
35454
- "Checkout sync is not configured in forum.yaml. Use fm config sync add --target <path> to enable local file sync."
35917
+ "Checkout sync is not configured in .forum/sync.yaml. Use fm config sync add --target <path> to enable local file sync."
35455
35918
  );
35456
35919
  };
35457
35920
  var warnReadonlyCheckoutPush = (checkout, force) => {
@@ -35459,7 +35922,7 @@ var warnReadonlyCheckoutPush = (checkout, force) => {
35459
35922
  return;
35460
35923
  }
35461
35924
  renderWarning(
35462
- "Warning: forum.yaml has sync.readonly enabled. This manual push can write local files back to the store. Rerun with --force to bypass this reminder."
35925
+ "Warning: .forum/sync.yaml has sync.readonly enabled. This manual push can write local files back to the store. Rerun with --force to bypass this reminder."
35463
35926
  );
35464
35927
  };
35465
35928
  var referenceStructuralChangeLabel = (detail) => {
@@ -35518,20 +35981,26 @@ var confirmCheckoutReferenceStructuralWrites = async ({
35518
35981
  yes
35519
35982
  }) => {
35520
35983
  const result = operation === "sync" ? await syncCheckout({
35984
+ ignoreSettings: "sync",
35521
35985
  client,
35522
35986
  dryRun: true,
35523
35987
  force,
35524
35988
  ignorePatterns: checkout.ignore,
35989
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
35990
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35525
35991
  localPath: checkout.rootDir,
35526
35992
  progress,
35527
35993
  storeRef: {},
35528
35994
  syncRules: checkout.syncRules
35529
35995
  }) : await pushCheckout({
35996
+ ignoreSettings: "preview",
35530
35997
  client,
35531
35998
  dryRun: true,
35532
35999
  filePaths,
35533
36000
  force,
35534
36001
  ignorePatterns: checkout.ignore,
36002
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36003
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35535
36004
  localPath: checkout.rootDir,
35536
36005
  progress,
35537
36006
  storeRef: {},
@@ -35576,12 +36045,18 @@ var runCheckoutOperation = async ({
35576
36045
  filePaths,
35577
36046
  force,
35578
36047
  ignorePatterns: checkout.ignore,
36048
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36049
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35579
36050
  localPath: checkout.rootDir,
35580
36051
  progress,
35581
36052
  storeRef: {},
35582
36053
  syncRules: checkout.syncRules
35583
36054
  };
35584
- const result = operation === "pull" ? await pullCheckout(commonInput) : await pushCheckout({ ...commonInput, dryRun });
36055
+ const result = operation === "pull" ? await pullCheckout({ ...commonInput, ignoreSettings: "preview" }) : await pushCheckout({
36056
+ ignoreSettings: "preview",
36057
+ ...commonInput,
36058
+ dryRun
36059
+ });
35585
36060
  formatCheckoutResult(labels.summaryVerb, result, {
35586
36061
  detailsRendered: Boolean(progress),
35587
36062
  dryRun,
@@ -35675,6 +36150,7 @@ var assertCheckoutReadyForReadOnlyPull = async (checkout) => {
35675
36150
  };
35676
36151
  var runCheckoutPullThenPush = async ({
35677
36152
  checkout,
36153
+ conflictPolicy,
35678
36154
  client,
35679
36155
  force,
35680
36156
  verbose,
@@ -35692,6 +36168,8 @@ var runCheckoutPullThenPush = async ({
35692
36168
  }
35693
36169
  progress?.start?.();
35694
36170
  const result = await syncCheckout({
36171
+ ignoreSettings: "sync",
36172
+ conflictPolicy,
35695
36173
  beforeRuleApply: async ({ result: rulePlan }) => {
35696
36174
  await confirmReferenceStructuralWrites(
35697
36175
  referenceStructuralWriteDetails(rulePlan),
@@ -35701,6 +36179,8 @@ var runCheckoutPullThenPush = async ({
35701
36179
  client,
35702
36180
  force,
35703
36181
  ignorePatterns: checkout.ignore,
36182
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36183
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35704
36184
  localPath: checkout.rootDir,
35705
36185
  progress,
35706
36186
  storeRef: {},
@@ -35727,10 +36207,13 @@ var assertGitSyncHasNoPushPlan = async ({
35727
36207
  force
35728
36208
  }) => {
35729
36209
  const result = await pushCheckout({
36210
+ ignoreSettings: "preview",
35730
36211
  client,
35731
36212
  dryRun: true,
35732
36213
  force,
35733
36214
  ignorePatterns: checkout.ignore,
36215
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36216
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35734
36217
  localPath: checkout.rootDir,
35735
36218
  storeRef: {},
35736
36219
  syncRules: checkout.syncRules
@@ -35754,16 +36237,6 @@ var assertGitSyncHasNoPushPlan = async ({
35754
36237
  ].join("\n")
35755
36238
  );
35756
36239
  };
35757
- var refreshStoreIgnoreCache = async (checkout, client) => {
35758
- const storeRef = await resolveProjectStoreRef();
35759
- if (!storeRef) {
35760
- return null;
35761
- }
35762
- const metadata = await client.readStoreByRef(storeRef);
35763
- const storeIgnore = metadata?.ignore ?? [];
35764
- await writeStoreIgnoreCache(checkout.rootDir, storeIgnore);
35765
- return { storeIgnore, storeRef };
35766
- };
35767
36240
  var analyzeLocalIgnore = async (checkout) => {
35768
36241
  const [files, presentMarkers, gitignorePatterns] = await Promise.all([
35769
36242
  collectLocalCheckoutFiles(checkout.rootDir, []),
@@ -35777,7 +36250,7 @@ var analyzeLocalIgnore = async (checkout) => {
35777
36250
  });
35778
36251
  };
35779
36252
  var promptIgnoreAction = async () => {
35780
- const readline = createInterface({
36253
+ const readline = createInterface2({
35781
36254
  input: process.stdin,
35782
36255
  output: process.stdout
35783
36256
  });
@@ -35794,17 +36267,30 @@ var promptIgnoreAction = async () => {
35794
36267
  };
35795
36268
  var isInteractiveTty = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
35796
36269
  var runIgnoreSetupGate = async (checkout, client, assumeYes) => {
35797
- const refreshed = await refreshStoreIgnoreCache(checkout, client);
35798
- if (!refreshed) {
35799
- return;
35800
- }
35801
- const { storeIgnore, storeRef } = refreshed;
35802
- if (storeIgnore.length > 0) {
35803
- return;
36270
+ const storeRef = await resolveProjectStoreRef();
36271
+ if (!storeRef) return;
36272
+ const sharedFilePath = sharedIgnorePath(
36273
+ checkout.rootDir,
36274
+ `${storeRef.workspaceSlug}/${storeRef.storeSlug}`
36275
+ );
36276
+ let storeIgnore = await readSharedIgnoreFile(sharedFilePath);
36277
+ if (storeIgnore === null) {
36278
+ const metadata = await client.readStoreByRef(storeRef);
36279
+ if (!metadata) throw new Error("Store not found for ignore setup.");
36280
+ const restored = await reconcileSharedIgnoreFile({
36281
+ rootDir: checkout.rootDir,
36282
+ sharedFilePath,
36283
+ store: `${storeRef.workspaceSlug}/${storeRef.storeSlug}`,
36284
+ remote: metadata.ignore ?? [],
36285
+ publish: async () => {
36286
+ }
36287
+ });
36288
+ storeIgnore = restored.patterns;
35804
36289
  }
36290
+ if (storeIgnore.length > 0) return;
35805
36291
  await maybePromptIgnoreSetup({
35806
36292
  assumeYes,
35807
- client,
36293
+ writePatterns: (patterns) => writeSharedIgnoreFile(sharedFilePath, patterns),
35808
36294
  analysis: await analyzeLocalIgnore(checkout),
35809
36295
  onboarded: await readIgnoreOnboardedAt(checkout.rootDir) !== null,
35810
36296
  currentPatterns: storeIgnore,
@@ -35812,11 +36298,11 @@ var runIgnoreSetupGate = async (checkout, client, assumeYes) => {
35812
36298
  chooseAction: promptIgnoreAction,
35813
36299
  markOnboarded: () => markIgnoreOnboarded(checkout.rootDir),
35814
36300
  renderOutput,
35815
- renderWarning,
35816
- storeRef
36301
+ renderWarning
35817
36302
  });
35818
36303
  };
35819
36304
  var runCheckoutSyncOnce = async ({
36305
+ conflictPolicy,
35820
36306
  client,
35821
36307
  force,
35822
36308
  verbose,
@@ -35824,17 +36310,27 @@ var runCheckoutSyncOnce = async ({
35824
36310
  }) => {
35825
36311
  const checkout = await resolveCheckoutConfig();
35826
36312
  assertCheckoutSyncEnabled(checkout);
35827
- await runCheckoutPullThenPush({ checkout, client, force, verbose, yes });
36313
+ await runCheckoutPullThenPush({
36314
+ checkout,
36315
+ client,
36316
+ conflictPolicy,
36317
+ force,
36318
+ verbose,
36319
+ yes
36320
+ });
35828
36321
  };
35829
36322
  var DEFAULT_FULL_SYNC_INTERVAL_MS = 5 * 60 * 1e3;
35830
36323
  var runCheckoutSyncCollecting = async ({
35831
36324
  checkout,
36325
+ conflictPolicy,
35832
36326
  client,
35833
36327
  force,
35834
36328
  yes = false
35835
36329
  }) => {
35836
36330
  assertCheckoutSyncEnabled(checkout);
35837
36331
  return syncCheckout({
36332
+ ignoreSettings: "sync",
36333
+ conflictPolicy,
35838
36334
  beforeRuleApply: async ({ result }) => {
35839
36335
  await confirmReferenceStructuralWrites(
35840
36336
  referenceStructuralWriteDetails(result),
@@ -35844,6 +36340,8 @@ var runCheckoutSyncCollecting = async ({
35844
36340
  client,
35845
36341
  force,
35846
36342
  ignorePatterns: checkout.ignore,
36343
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36344
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35847
36345
  localPath: checkout.rootDir,
35848
36346
  storeRef: {},
35849
36347
  syncRules: checkout.syncRules
@@ -35870,14 +36368,15 @@ var runCheckoutStatus = async ({
35870
36368
  }) => {
35871
36369
  const initialCheckout = await resolveCheckoutConfig();
35872
36370
  assertCheckoutSyncEnabled(initialCheckout);
35873
- await refreshStoreIgnoreCache(initialCheckout, client);
35874
- const checkout = await resolveCheckoutConfig();
35875
- assertCheckoutSyncEnabled(checkout);
36371
+ const checkout = initialCheckout;
35876
36372
  const pullResult = await pullCheckout({
36373
+ ignoreSettings: "preview",
35877
36374
  client,
35878
36375
  dryRun: true,
35879
36376
  force,
35880
36377
  ignorePatterns: checkout.ignore,
36378
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36379
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35881
36380
  localPath: checkout.rootDir,
35882
36381
  storeRef: {},
35883
36382
  syncRules: checkout.syncRules
@@ -35920,10 +36419,13 @@ var runCheckoutStatus = async ({
35920
36419
  return;
35921
36420
  }
35922
36421
  const pushResult = await pushCheckout({
36422
+ ignoreSettings: "preview",
35923
36423
  client,
35924
36424
  dryRun: true,
35925
36425
  force,
35926
36426
  ignorePatterns: checkout.ignore,
36427
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36428
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35927
36429
  localPath: checkout.rootDir,
35928
36430
  storeRef: {},
35929
36431
  syncRules: checkout.syncRules
@@ -35936,18 +36438,18 @@ var runCheckoutStatus = async ({
35936
36438
  });
35937
36439
  };
35938
36440
  var isCheckoutStateGitPath = isCheckoutStateDirectoryPath;
35939
- var checkoutFilesManifestPath2 = (checkoutRootDir) => `${resolveCheckoutStateDirectoryName(checkoutRootDir)}/files.json`;
36441
+ var checkoutFilesManifestPath2 = () => `${resolveCheckoutStateDirectoryName()}/files.json`;
35940
36442
  var isInsideOrEqual = (parentPath, childPath2) => {
35941
- const relativePath = path19.relative(parentPath, childPath2);
35942
- return relativePath === "" || !relativePath.startsWith("..") && !path19.isAbsolute(relativePath);
36443
+ const relativePath = path21.relative(parentPath, childPath2);
36444
+ return relativePath === "" || !relativePath.startsWith("..") && !path21.isAbsolute(relativePath);
35943
36445
  };
35944
36446
  var resolveSyncTargetPaths = (checkout) => checkout.syncRules.map(
35945
- (rule) => path19.resolve(checkout.rootDir, rule.target || ".")
36447
+ (rule) => path21.resolve(checkout.rootDir, rule.target || ".")
35946
36448
  );
35947
- var resolveCheckoutManifestGitPath = (checkout, gitRoot) => path19.relative(
36449
+ var resolveCheckoutManifestGitPath = (checkout, gitRoot) => path21.relative(
35948
36450
  gitRoot,
35949
- path19.join(checkout.rootDir, checkoutFilesManifestPath2(checkout.rootDir))
35950
- ) || checkoutFilesManifestPath2(checkout.rootDir);
36451
+ path21.join(checkout.rootDir, checkoutFilesManifestPath2())
36452
+ ) || checkoutFilesManifestPath2();
35951
36453
  var stageCheckoutManifest = async ({
35952
36454
  checkout,
35953
36455
  gitRoot
@@ -35972,7 +36474,7 @@ var assertGitChangesStayInSyncTargets = async ({
35972
36474
  if (isCheckoutStateGitPath(entry.path)) {
35973
36475
  return false;
35974
36476
  }
35975
- const absolutePath = path19.resolve(gitRoot, entry.path);
36477
+ const absolutePath = path21.resolve(gitRoot, entry.path);
35976
36478
  return !allowedPaths.some(
35977
36479
  (allowedPath) => isInsideOrEqual(allowedPath, absolutePath)
35978
36480
  );
@@ -35994,7 +36496,7 @@ var stageGitSyncTargets = async ({
35994
36496
  }) => {
35995
36497
  const targetPaths = [];
35996
36498
  for (const targetPath of resolveSyncTargetPaths(checkout)) {
35997
- const relativePath = path19.relative(gitRoot, targetPath) || ".";
36499
+ const relativePath = path21.relative(gitRoot, targetPath) || ".";
35998
36500
  const tracked = await runGit(gitRoot, ["ls-files", "--", relativePath]);
35999
36501
  if (await pathExists3(targetPath) || tracked.stdout.trim()) {
36000
36502
  targetPaths.push(relativePath);
@@ -36036,7 +36538,7 @@ var runGitSyncCommand = async ({
36036
36538
  assertCheckoutSyncEnabled(checkout);
36037
36539
  if (!checkout.gitSyncCommitMessage) {
36038
36540
  throw new Error(
36039
- "No Git sync commit message configured. Add git.commit-message to forum.yaml."
36541
+ "No Git sync commit message configured. Add git.commit-message to .forum/sync.yaml."
36040
36542
  );
36041
36543
  }
36042
36544
  const gitRoot = await resolveGitRoot(checkout.rootDir);
@@ -36069,6 +36571,7 @@ var runCheckoutReadOnlyPullOnce = async ({
36069
36571
  };
36070
36572
  var buildCheckoutWatchEntry = ({
36071
36573
  checkout,
36574
+ conflictPolicy,
36072
36575
  client,
36073
36576
  force,
36074
36577
  yes
@@ -36077,10 +36580,17 @@ var buildCheckoutWatchEntry = ({
36077
36580
  rootDir: checkout.rootDir,
36078
36581
  startMessage: `Watching ${checkout.rootDir}`,
36079
36582
  storeRefs: changesFeedStoreRefsForCheckout(checkout),
36080
- syncOnce: () => runCheckoutSyncCollecting({ checkout, client, force, yes })
36583
+ syncOnce: async () => runCheckoutSyncCollecting({
36584
+ checkout: await resolveCheckoutConfig(checkout.rootDir),
36585
+ client,
36586
+ conflictPolicy,
36587
+ force,
36588
+ yes
36589
+ })
36081
36590
  });
36082
36591
  var resolveExplicitCheckoutEntry = async ({
36083
36592
  checkoutPath,
36593
+ conflictPolicy,
36084
36594
  client,
36085
36595
  force,
36086
36596
  yes
@@ -36088,13 +36598,20 @@ var resolveExplicitCheckoutEntry = async ({
36088
36598
  const checkout = await resolveCheckoutConfig(checkoutPath);
36089
36599
  if (checkout.rootDir !== checkoutPath) {
36090
36600
  throw new Error(
36091
- `${checkoutPath} is not a Forum checkout (no forum.yaml here).`
36601
+ `${checkoutPath} is not a Forum checkout (no .forum/sync.yaml here).`
36092
36602
  );
36093
36603
  }
36094
36604
  assertCheckoutSyncEnabled(checkout);
36095
- return buildCheckoutWatchEntry({ checkout, client, force, yes });
36605
+ return buildCheckoutWatchEntry({
36606
+ checkout,
36607
+ client,
36608
+ conflictPolicy,
36609
+ force,
36610
+ yes
36611
+ });
36096
36612
  };
36097
36613
  var resolveDiscoveredCheckoutEntries = async ({
36614
+ conflictPolicy,
36098
36615
  client,
36099
36616
  force,
36100
36617
  rootPath,
@@ -36110,7 +36627,9 @@ var resolveDiscoveredCheckoutEntries = async ({
36110
36627
  if (checkout.syncRules.length === 0) {
36111
36628
  continue;
36112
36629
  }
36113
- entries.push(buildCheckoutWatchEntry({ checkout, client, force, yes }));
36630
+ entries.push(
36631
+ buildCheckoutWatchEntry({ checkout, client, conflictPolicy, force, yes })
36632
+ );
36114
36633
  }
36115
36634
  if (entries.length === 0) {
36116
36635
  throw new Error(
@@ -36120,6 +36639,7 @@ var resolveDiscoveredCheckoutEntries = async ({
36120
36639
  return entries;
36121
36640
  };
36122
36641
  var runCheckoutSyncWatch = async ({
36642
+ conflictPolicy,
36123
36643
  client,
36124
36644
  force,
36125
36645
  intervalMs,
@@ -36192,12 +36712,30 @@ var runCheckoutSyncWatch = async ({
36192
36712
  { watchPaths, pathsFile },
36193
36713
  {
36194
36714
  readPathsFile,
36195
- resolveDiscoveredEntries: (rootPath) => resolveDiscoveredCheckoutEntries({ client, force, rootPath, yes }),
36196
- resolveExplicitEntry: (checkoutPath) => resolveExplicitCheckoutEntry({ checkoutPath, client, force, yes }),
36715
+ resolveDiscoveredEntries: (rootPath) => resolveDiscoveredCheckoutEntries({
36716
+ client,
36717
+ conflictPolicy,
36718
+ force,
36719
+ rootPath,
36720
+ yes
36721
+ }),
36722
+ resolveExplicitEntry: (checkoutPath) => resolveExplicitCheckoutEntry({
36723
+ checkoutPath,
36724
+ client,
36725
+ conflictPolicy,
36726
+ force,
36727
+ yes
36728
+ }),
36197
36729
  resolveSingleCheckoutEntry: async () => {
36198
36730
  const checkout = await resolveCheckoutConfig();
36199
36731
  assertCheckoutSyncEnabled(checkout);
36200
- return buildCheckoutWatchEntry({ checkout, client, force, yes });
36732
+ return buildCheckoutWatchEntry({
36733
+ checkout,
36734
+ client,
36735
+ conflictPolicy,
36736
+ force,
36737
+ yes
36738
+ });
36201
36739
  }
36202
36740
  }
36203
36741
  );
@@ -36254,11 +36792,11 @@ var resolveReadableStore = async (storeInput, client) => {
36254
36792
  var writeCheckoutConfig = async (directoryPath, metadata, workspaceSlug, options2 = {}) => {
36255
36793
  if (!workspaceSlug) {
36256
36794
  throw new Error(
36257
- `Store ${metadata.slug} is missing a workspace slug and cannot be written to forum.yaml.`
36795
+ `Store ${metadata.slug} is missing a workspace slug and cannot be written to .forum/sync.yaml.`
36258
36796
  );
36259
36797
  }
36260
36798
  await writeForumProjectConfig(
36261
- path19.join(directoryPath, defaultProjectConfigFilename),
36799
+ path21.join(directoryPath, defaultProjectConfigFilename),
36262
36800
  {
36263
36801
  syncRules: [
36264
36802
  {
@@ -36274,6 +36812,18 @@ var writeCheckoutConfig = async (directoryPath, metadata, workspaceSlug, options
36274
36812
  ]
36275
36813
  }
36276
36814
  );
36815
+ await reconcileSharedIgnoreFile({
36816
+ rootDir: directoryPath,
36817
+ sharedFilePath: sharedIgnorePath(
36818
+ directoryPath,
36819
+ `${workspaceSlug}/${metadata.slug}`
36820
+ ),
36821
+ store: `${workspaceSlug}/${metadata.slug}`,
36822
+ remote: metadata.ignore ?? [],
36823
+ publish: async () => {
36824
+ throw new Error("Initialization cannot publish ignore rules.");
36825
+ }
36826
+ });
36277
36827
  };
36278
36828
  var normalizeProjectConfigPath = (value, fieldName, allowCurrentDirectory = false) => {
36279
36829
  const trimmed = value.trim().replace(/^\/+/, "").replace(/^\.\/+/, "").replace(/\/+$/, "");
@@ -36283,7 +36833,7 @@ var normalizeProjectConfigPath = (value, fieldName, allowCurrentDirectory = fals
36283
36833
  }
36284
36834
  throw new Error(`${fieldName} must be a non-empty relative path.`);
36285
36835
  }
36286
- if (path19.isAbsolute(value)) {
36836
+ if (path21.isAbsolute(value)) {
36287
36837
  throw new Error(`${fieldName} must be a relative path.`);
36288
36838
  }
36289
36839
  const segments2 = trimmed.split("/");
@@ -36336,7 +36886,7 @@ var runProjectConfigSyncCommand = async (mode, args) => {
36336
36886
  const projectConfig = await findForumProjectConfig();
36337
36887
  if (!projectConfig) {
36338
36888
  throw new Error(
36339
- "No forum.yaml found. Run fm init <store> before configuring project sync."
36889
+ "No .forum/sync.yaml found. Run fm init <store> before configuring project sync."
36340
36890
  );
36341
36891
  }
36342
36892
  if (mode === "clear") {
@@ -36439,7 +36989,7 @@ var runProjectConfigSyncCommand = async (mode, args) => {
36439
36989
  var runProjectConfigValidateCommand = async () => {
36440
36990
  const projectConfig = await findForumProjectConfig();
36441
36991
  if (!projectConfig) {
36442
- throw new Error("No forum.yaml found.");
36992
+ throw new Error("No .forum/sync.yaml found.");
36443
36993
  }
36444
36994
  renderOutput(`Valid Forum config: ${projectConfig.path}`);
36445
36995
  renderOutput(
@@ -36449,7 +36999,7 @@ var runProjectConfigValidateCommand = async () => {
36449
36999
  var runProjectConfigStandardizeCommand = async () => {
36450
37000
  const projectConfig = await findForumProjectConfig();
36451
37001
  if (!projectConfig) {
36452
- throw new Error("No forum.yaml found.");
37002
+ throw new Error("No .forum/sync.yaml found.");
36453
37003
  }
36454
37004
  await writeForumProjectConfig(projectConfig.path, {
36455
37005
  defaultStore: projectConfig.defaultStore,
@@ -36465,7 +37015,7 @@ var defaultCloneDirectoryName = (storeId) => {
36465
37015
  var fsErrorCode = (error51) => error51 instanceof Error && "code" in error51 ? String(error51.code ?? "") : "";
36466
37016
  var ensureGitMetadataExists = async (directoryPath) => {
36467
37017
  try {
36468
- await lstat2(path19.join(directoryPath, ".git"));
37018
+ await lstat2(path21.join(directoryPath, ".git"));
36469
37019
  } catch (error51) {
36470
37020
  if (fsErrorCode(error51) === "ENOENT") {
36471
37021
  throw new Error(
@@ -36503,8 +37053,8 @@ var confirmGitMetadataDeletion = async (localPath, assumeYes) => {
36503
37053
  "Refusing to delete .git without interactive confirmation. Rerun with --yes to confirm in a non-interactive shell."
36504
37054
  );
36505
37055
  }
36506
- const gitPath = path19.join(localPath, ".git");
36507
- const gitignorePath = path19.join(localPath, ".gitignore");
37056
+ const gitPath = path21.join(localPath, ".git");
37057
+ const gitignorePath = path21.join(localPath, ".gitignore");
36508
37058
  const confirmationPhrase = "DELETE GIT";
36509
37059
  renderWarning("");
36510
37060
  renderWarning("DANGER: Forum is about to remove Git from this checkout.");
@@ -36516,7 +37066,7 @@ var confirmGitMetadataDeletion = async (localPath, assumeYes) => {
36516
37066
  renderWarning(
36517
37067
  "Only continue if the repository history is backed up or you are certain you no longer need it here."
36518
37068
  );
36519
- const readline = createInterface({
37069
+ const readline = createInterface2({
36520
37070
  input: process.stdin,
36521
37071
  output: process.stdout
36522
37072
  });
@@ -36546,11 +37096,11 @@ var runGitConvertCommand = async ({
36546
37096
  const { created, metadata, workspaceSlug, storeRef } = await resolveOrCreateConversionStore(storeInput, client);
36547
37097
  if (!workspaceSlug) {
36548
37098
  throw new Error(
36549
- `Store ${metadata.slug} is missing a workspace slug and cannot be written to forum.yaml.`
37099
+ `Store ${metadata.slug} is missing a workspace slug and cannot be written to .forum/sync.yaml.`
36550
37100
  );
36551
37101
  }
36552
37102
  await writeForumProjectConfig(
36553
- path19.join(localPath, defaultProjectConfigFilename),
37103
+ path21.join(localPath, defaultProjectConfigFilename),
36554
37104
  {
36555
37105
  syncRules: [
36556
37106
  {
@@ -36565,14 +37115,24 @@ var runGitConvertCommand = async ({
36565
37115
  ]
36566
37116
  }
36567
37117
  );
36568
- if (ignorePatterns.length > 0) {
36569
- await client.upsertStoreByRef({
36570
- ...storeRef,
36571
- patch: { ignore: ignorePatterns }
36572
- });
36573
- }
37118
+ await reconcileSharedIgnoreFile({
37119
+ rootDir: localPath,
37120
+ sharedFilePath: sharedIgnorePath(
37121
+ localPath,
37122
+ `${workspaceSlug}/${metadata.slug}`
37123
+ ),
37124
+ store: `${workspaceSlug}/${metadata.slug}`,
37125
+ remote: metadata.ignore ?? [],
37126
+ publish: async () => {
37127
+ }
37128
+ });
37129
+ await writeSharedIgnoreFile(
37130
+ sharedIgnorePath(localPath, `${workspaceSlug}/${metadata.slug}`),
37131
+ [...metadata.ignore ?? [], ...ignorePatterns]
37132
+ );
36574
37133
  const conversionIgnorePatterns = [...ignorePatterns, ".gitignore"];
36575
37134
  await pushCheckout({
37135
+ ignoreSettings: "preview",
36576
37136
  client,
36577
37137
  dryRun: true,
36578
37138
  force,
@@ -36581,6 +37141,7 @@ var runGitConvertCommand = async ({
36581
37141
  storeRef
36582
37142
  });
36583
37143
  const result = await pushCheckout({
37144
+ ignoreSettings: "preview",
36584
37145
  client,
36585
37146
  force,
36586
37147
  ignorePatterns: conversionIgnorePatterns,
@@ -36601,8 +37162,8 @@ var runGitConvertCommand = async ({
36601
37162
  "Aborted before deleting Git metadata. The Forum push completed, but .git and .gitignore were left in place."
36602
37163
  );
36603
37164
  }
36604
- await rm4(path19.join(localPath, ".gitignore"), { force: true });
36605
- await rm4(path19.join(localPath, ".git"), { recursive: true, force: true });
37165
+ await rm5(path21.join(localPath, ".gitignore"), { force: true });
37166
+ await rm5(path21.join(localPath, ".git"), { recursive: true, force: true });
36606
37167
  renderOutput(
36607
37168
  `${created ? "Created" : "Using existing"} store ${metadata.slug}`
36608
37169
  );
@@ -36641,7 +37202,7 @@ var main = async (argv = process.argv.slice(2)) => {
36641
37202
  const config3 = await resolveConfig(parsed);
36642
37203
  if (subcommand === "login") {
36643
37204
  const apiBaseUrl2 = ensureApiBaseUrl(config3.apiBaseUrl);
36644
- const token2 = await withActivity(
37205
+ const token = await withActivity(
36645
37206
  "Starting login",
36646
37207
  () => interactiveLogin({
36647
37208
  apiBaseUrl: apiBaseUrl2,
@@ -36649,7 +37210,7 @@ var main = async (argv = process.argv.slice(2)) => {
36649
37210
  })
36650
37211
  );
36651
37212
  await writeConfig(parsed.stateDir, {
36652
- token: token2,
37213
+ token,
36653
37214
  pollIntervalMs: config3.pollIntervalMs,
36654
37215
  forceWrites: config3.forceWrites
36655
37216
  });
@@ -36720,7 +37281,7 @@ var main = async (argv = process.argv.slice(2)) => {
36720
37281
  }
36721
37282
  if (command === "hash" && parsed.local) {
36722
37283
  const filePath = rest[0];
36723
- renderOutput(await hashLocalFile(path19.resolve(filePath)));
37284
+ renderOutput(await hashLocalFile(path21.resolve(filePath)));
36724
37285
  return;
36725
37286
  }
36726
37287
  const config2 = await resolveConfig(parsed);
@@ -36746,8 +37307,7 @@ var main = async (argv = process.argv.slice(2)) => {
36746
37307
  }
36747
37308
  }
36748
37309
  const apiBaseUrl = ensureApiBaseUrl(config2.apiBaseUrl);
36749
- const token = ensureToken(config2.token);
36750
- const client = createClient({ ...config2, apiBaseUrl, token });
37310
+ const client = createClient({ ...config2, apiBaseUrl });
36751
37311
  if (command === "encrypt") {
36752
37312
  await runEncryptCommand(rest, parsed, client);
36753
37313
  return;
@@ -36995,26 +37555,45 @@ var main = async (argv = process.argv.slice(2)) => {
36995
37555
  const subcommand = rest[0];
36996
37556
  const checkout = await resolveCheckoutConfig();
36997
37557
  assertCheckoutSyncEnabled(checkout);
36998
- const storeRef = await resolveProjectStoreRef();
36999
- if (!storeRef) {
37558
+ const stores = [...new Set(checkout.syncRules.map((rule2) => rule2.store))];
37559
+ const requested = parsed.storeFilters;
37560
+ const candidates = requested.length ? stores.filter(
37561
+ (store2) => requested.includes(store2) || requested.includes(store2.split("/").at(-1))
37562
+ ) : stores;
37563
+ if (candidates.length !== 1)
37000
37564
  throw new Error(
37001
- "fm ignore needs a single-store checkout (run it inside a fm clone/init)."
37565
+ "Select one configured store with fm ignore --store workspace/store."
37002
37566
  );
37567
+ const store = candidates[0];
37568
+ const rule = checkout.syncRules.find((entry) => entry.store === store);
37569
+ const sharedFilePath = sharedIgnorePath(checkout.rootDir, store);
37570
+ let currentPatterns = await readSharedIgnoreFile(sharedFilePath);
37571
+ if (currentPatterns === null) {
37572
+ const metadata = await client.readStoreByRef({
37573
+ workspaceSlug: rule.storeOwner,
37574
+ storeSlug: rule.storeSlug
37575
+ });
37576
+ if (!metadata) throw new Error(`Store not found: ${store}`);
37577
+ await reconcileSharedIgnoreFile({
37578
+ rootDir: checkout.rootDir,
37579
+ sharedFilePath,
37580
+ store,
37581
+ remote: metadata.ignore ?? [],
37582
+ publish: async () => {
37583
+ }
37584
+ });
37585
+ currentPatterns = metadata.ignore ?? [];
37003
37586
  }
37004
- const metadata = await client.readStoreByRef(storeRef);
37005
- const currentPatterns = metadata?.ignore ?? [];
37006
37587
  await runIgnoreCommand({
37007
- checkout,
37008
- client,
37588
+ writePatterns: (patterns) => writeSharedIgnoreFile(sharedFilePath, patterns),
37009
37589
  commandNames,
37010
37590
  confirmYesNo,
37011
- currentPatterns,
37591
+ currentPatterns: currentPatterns ?? [],
37012
37592
  analyzeLocal: () => analyzeLocalIgnore(checkout),
37013
37593
  markOnboarded: () => markIgnoreOnboarded(checkout.rootDir),
37014
37594
  pattern: rest[1],
37015
37595
  patterns: rest.slice(1),
37016
37596
  renderOutput,
37017
- storeRef,
37018
37597
  subcommand,
37019
37598
  assumeYes: parsed.yes
37020
37599
  });
@@ -37093,7 +37672,7 @@ var main = async (argv = process.argv.slice(2)) => {
37093
37672
  }
37094
37673
  const checkout = await resolveCheckoutConfig();
37095
37674
  assertCheckoutSyncEnabled(checkout);
37096
- await runIgnoreSetupGate(checkout, client, parsed.yes);
37675
+ if (!parsed.dryRun) await runIgnoreSetupGate(checkout, client, parsed.yes);
37097
37676
  await runCheckoutPush({
37098
37677
  checkout,
37099
37678
  client,
@@ -37106,12 +37685,41 @@ var main = async (argv = process.argv.slice(2)) => {
37106
37685
  return;
37107
37686
  }
37108
37687
  if (command === "sync") {
37688
+ if (parsed.conflictPolicy && (parsed.force || parsed.readonly)) {
37689
+ throw new Error(
37690
+ "--conflict cannot be combined with --force or --readonly."
37691
+ );
37692
+ }
37109
37693
  if (rest[0] === "reset") {
37110
37694
  const checkout = await resolveCheckoutConfig();
37111
37695
  await resetCheckoutSyncState(checkout.rootDir);
37112
37696
  renderOutput("Cleared local checkout sync state.");
37113
37697
  return;
37114
37698
  }
37699
+ if (parsed.dryRun && parsed.conflictPolicy) {
37700
+ const checkout = await resolveCheckoutConfig();
37701
+ assertCheckoutSyncEnabled(checkout);
37702
+ const result = await syncCheckout({
37703
+ ignoreSettings: "sync",
37704
+ client,
37705
+ conflictPolicy: parsed.conflictPolicy,
37706
+ dryRun: true,
37707
+ force: false,
37708
+ ignorePatterns: checkout.ignore,
37709
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
37710
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
37711
+ localPath: checkout.rootDir,
37712
+ storeRef: {},
37713
+ syncRules: checkout.syncRules
37714
+ });
37715
+ if (parsed.json) renderOutput({ type: "checkout-sync-plan", ...result });
37716
+ else
37717
+ formatCheckoutResult("Planned", result, {
37718
+ syncRules: checkout.syncRules,
37719
+ verbose: parsed.verbose
37720
+ });
37721
+ return;
37722
+ }
37115
37723
  if (parsed.dryRun) {
37116
37724
  await withActivity(
37117
37725
  "Checking status",
@@ -37125,6 +37733,7 @@ var main = async (argv = process.argv.slice(2)) => {
37125
37733
  }
37126
37734
  if (parsed.watch) {
37127
37735
  await runCheckoutSyncWatch({
37736
+ conflictPolicy: parsed.conflictPolicy,
37128
37737
  client,
37129
37738
  force: parsed.force,
37130
37739
  intervalMs: config2.pollIntervalMs,
@@ -37152,6 +37761,7 @@ var main = async (argv = process.argv.slice(2)) => {
37152
37761
  }
37153
37762
  await runIgnoreSetupGate(await resolveCheckoutConfig(), client, parsed.yes);
37154
37763
  await runCheckoutSyncOnce({
37764
+ conflictPolicy: parsed.conflictPolicy,
37155
37765
  client,
37156
37766
  force: parsed.force,
37157
37767
  verbose: parsed.verbose,
@@ -37193,8 +37803,8 @@ var main = async (argv = process.argv.slice(2)) => {
37193
37803
  `Usage: ${commandNames} clone <storeId> [localPath] [--readonly]`
37194
37804
  );
37195
37805
  }
37196
- const localPath = path19.resolve(
37197
- rest[1] ?? path19.join(process.cwd(), defaultCloneDirectoryName(remoteInput))
37806
+ const localPath = path21.resolve(
37807
+ rest[1] ?? path21.join(process.cwd(), defaultCloneDirectoryName(remoteInput))
37198
37808
  );
37199
37809
  const { metadata, workspaceSlug, storeRef } = await resolveReadableStore(
37200
37810
  remoteInput,
@@ -37212,14 +37822,17 @@ var main = async (argv = process.argv.slice(2)) => {
37212
37822
  verbose: parsed.verbose
37213
37823
  });
37214
37824
  const result = await pullCheckout({
37825
+ ignoreSettings: "preview",
37215
37826
  client,
37216
37827
  ignorePatterns: checkout.ignore,
37828
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
37829
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
37217
37830
  localPath: checkout.rootDir,
37218
37831
  progress,
37219
37832
  storeRef,
37220
37833
  syncRules: checkout.syncRules
37221
37834
  });
37222
- renderOutput(`Cloned store ${metadata.slug} to ${path19.resolve(localPath)}`);
37835
+ renderOutput(`Cloned store ${metadata.slug} to ${path21.resolve(localPath)}`);
37223
37836
  formatCheckoutResult("Pulled", result, {
37224
37837
  detailsRendered: Boolean(progress),
37225
37838
  verbose: parsed.verbose