@mp-lb/forum-cli 0.1.5 → 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,15 +1753,362 @@ 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
2114
  import { createInterface as createInterface2 } from "readline/promises";
@@ -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),
@@ -18434,10 +18781,15 @@ var pinnedStoreIdsValueSchema = external_exports.array(storePreferenceIdSchema);
18434
18781
  var recentStoresValueSchema = external_exports.array(recentStoreVisitSchema);
18435
18782
  var pinnedStoreIdsSchema = pinnedStoreIdsValueSchema.default([]);
18436
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
+ );
18437
18788
  var userPreferencesSchema = external_exports.object({
18438
18789
  id: storeOwnerIdSchema,
18439
18790
  pinnedStoreIds: pinnedStoreIdsSchema,
18440
18791
  recentStores: recentStoresSchema,
18792
+ createTypeLastUsedAt: createTypeLastUsedAtSchema.default({}),
18441
18793
  recentStoresEnabled: external_exports.boolean().default(true),
18442
18794
  showHomeWelcomeBanner: external_exports.boolean().default(true),
18443
18795
  showHiddenFiles: external_exports.boolean().default(false),
@@ -18457,6 +18809,7 @@ var userPreferencesSchema = external_exports.object({
18457
18809
  var userPreferencesUpdateSchema = external_exports.object({
18458
18810
  pinnedStoreIds: pinnedStoreIdsValueSchema,
18459
18811
  recentStores: recentStoresValueSchema,
18812
+ createTypeLastUsedAt: createTypeLastUsedAtSchema,
18460
18813
  recentStoresEnabled: external_exports.boolean(),
18461
18814
  showHomeWelcomeBanner: external_exports.boolean(),
18462
18815
  showHiddenFiles: external_exports.boolean(),
@@ -19189,21 +19542,21 @@ var isKey = (value) => /^\w*$/.test(value);
19189
19542
  var isUndefined = (val) => val === void 0;
19190
19543
  var compact = (value) => Array.isArray(value) ? value.filter(Boolean) : [];
19191
19544
  var stringToPath = (input) => compact(input.replace(/["|']|\]/g, "").split(/\.|\[/));
19192
- var get = (object2, path20, defaultValue) => {
19193
- if (!path20 || !isObject2(object2)) {
19545
+ var get = (object2, path22, defaultValue) => {
19546
+ if (!path22 || !isObject2(object2)) {
19194
19547
  return defaultValue;
19195
19548
  }
19196
- const paths = isKey(path20) ? [path20] : stringToPath(path20);
19549
+ const paths = isKey(path22) ? [path22] : stringToPath(path22);
19197
19550
  const result = paths.reduce((result2, key) => {
19198
19551
  return isNullOrUndefined(result2) ? void 0 : result2[key];
19199
19552
  }, object2);
19200
- 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;
19201
19554
  };
19202
19555
  var isBoolean = (value) => typeof value === "boolean";
19203
19556
  var isFunction = (value) => typeof value === "function";
19204
- var set2 = (object2, path20, value) => {
19557
+ var set2 = (object2, path22, value) => {
19205
19558
  let index = -1;
19206
- const tempPath = isKey(path20) ? [path20] : stringToPath(path20);
19559
+ const tempPath = isKey(path22) ? [path22] : stringToPath(path22);
19207
19560
  const length = tempPath.length;
19208
19561
  const lastIndex = length - 1;
19209
19562
  while (++index < length) {
@@ -19401,12 +19754,12 @@ function isEmptyArray(obj) {
19401
19754
  }
19402
19755
  return true;
19403
19756
  }
19404
- function unset(object2, path20) {
19405
- if (isString(path20) && Object.prototype.hasOwnProperty.call(object2, path20)) {
19406
- delete object2[path20];
19757
+ function unset(object2, path22) {
19758
+ if (isString(path22) && Object.prototype.hasOwnProperty.call(object2, path22)) {
19759
+ delete object2[path22];
19407
19760
  return object2;
19408
19761
  }
19409
- const paths = Array.isArray(path20) ? path20 : isKey(path20) ? [path20] : stringToPath(path20);
19762
+ const paths = Array.isArray(path22) ? path22 : isKey(path22) ? [path22] : stringToPath(path22);
19410
19763
  const childObject = paths.length === 1 ? object2 : baseGet(object2, paths);
19411
19764
  const index = paths.length - 1;
19412
19765
  const key = paths[index];
@@ -23131,7 +23484,7 @@ var publicActionSubmitResultSchema = external_exports.unknown();
23131
23484
 
23132
23485
  // ../../packages/core/src/widgetRuntime.ts
23133
23486
  var widgetRuntimeOutputPathSchema = storePathSchema.refine(
23134
- (path20) => path20 !== "/",
23487
+ (path22) => path22 !== "/",
23135
23488
  {
23136
23489
  message: "Path must not be the root directory."
23137
23490
  }
@@ -23181,7 +23534,7 @@ var widgetRuntimeCompleteAttachmentUploadInputSchema = storeRefSchema.safeExtend
23181
23534
  treeVisibility: external_exports.enum(["visible", "hidden"]).optional(),
23182
23535
  uploadIntentId: external_exports.string().trim().min(1)
23183
23536
  });
23184
- 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("//"), {
23185
23538
  message: "Link path must be a relative application path."
23186
23539
  });
23187
23540
  var widgetRuntimeCreateSigningLinkInputSchema = storeRefSchema.safeExtend({
@@ -23232,168 +23585,10 @@ var storeMemberSchema = external_exports.object({
23232
23585
  createdAt: external_exports.string().datetime().default(() => (/* @__PURE__ */ new Date()).toISOString())
23233
23586
  });
23234
23587
 
23235
- // ../../packages/core/src/storeIgnore.ts
23236
- var segments = (relativePath) => relativePath.split("/").filter(Boolean);
23237
- var basename = (relativePath) => {
23238
- const parts = segments(relativePath);
23239
- return parts[parts.length - 1] ?? "";
23240
- };
23241
- var dirRule = (name) => ({
23242
- label: name,
23243
- pattern: `${name}/`,
23244
- matches: (relativePath) => segments(relativePath).slice(0, -1).includes(name)
23245
- });
23246
- var ENV_EXAMPLE_SUFFIXES = [".example", ".sample", ".template", ".dist"];
23247
- var envRule = {
23248
- label: ".env",
23249
- pattern: ".env",
23250
- matches: (relativePath) => {
23251
- const name = basename(relativePath);
23252
- if (name !== ".env" && !name.startsWith(".env.")) {
23253
- return false;
23254
- }
23255
- return !ENV_EXAMPLE_SUFFIXES.some((suffix) => name.endsWith(suffix));
23256
- }
23257
- };
23258
- var logRule = {
23259
- label: "*.log",
23260
- pattern: "*.log",
23261
- matches: (relativePath) => basename(relativePath).endsWith(".log")
23262
- };
23263
- var vitepressRule = {
23264
- label: ".vitepress/cache",
23265
- pattern: ".vitepress/cache/",
23266
- matches: (relativePath) => relativePath.includes(".vitepress/cache/") || relativePath.includes(".vitepress/dist/")
23267
- };
23268
- var KNOWN_JUNK_RULES = [
23269
- dirRule("node_modules"),
23270
- dirRule("dist"),
23271
- dirRule("build"),
23272
- dirRule(".next"),
23273
- dirRule(".nuxt"),
23274
- dirRule(".svelte-kit"),
23275
- dirRule(".turbo"),
23276
- dirRule(".cache"),
23277
- dirRule("coverage"),
23278
- dirRule("target"),
23279
- dirRule("__pycache__"),
23280
- dirRule(".venv"),
23281
- vitepressRule,
23282
- envRule,
23283
- logRule
23284
- ];
23285
- var detectJunk = (paths, rules = KNOWN_JUNK_RULES) => {
23286
- const grouped = /* @__PURE__ */ new Map();
23287
- for (const path20 of paths) {
23288
- const rule = rules.find((candidate) => candidate.matches(path20));
23289
- if (!rule) continue;
23290
- const existing = grouped.get(rule);
23291
- if (existing) existing.push(path20);
23292
- else grouped.set(rule, [path20]);
23293
- }
23294
- return rules.filter((rule) => grouped.has(rule)).map((rule) => ({
23295
- rule,
23296
- paths: [...grouped.get(rule) ?? []].sort(
23297
- (left, right) => left < right ? -1 : left > right ? 1 : 0
23298
- )
23299
- }));
23300
- };
23301
- var IGNORE_PROPOSAL_RULES = [
23302
- { marker: "package.json", markerKind: "file", patterns: ["node_modules/"] },
23303
- { marker: ".vitepress", markerKind: "dir", patterns: [".vitepress/cache/"] },
23304
- { marker: "next.config.js", markerKind: "file", patterns: [".next/"] },
23305
- { marker: "next.config.mjs", markerKind: "file", patterns: [".next/"] },
23306
- { marker: "Cargo.toml", markerKind: "file", patterns: ["target/"] },
23307
- {
23308
- marker: "pyproject.toml",
23309
- markerKind: "file",
23310
- patterns: ["__pycache__/", ".venv/"]
23311
- }
23312
- ];
23313
- var analyzeIgnoreSignals = ({
23314
- presentPaths,
23315
- presentMarkers,
23316
- gitignorePatterns = [],
23317
- rules = KNOWN_JUNK_RULES,
23318
- proposalRules = IGNORE_PROPOSAL_RULES
23319
- }) => {
23320
- const findings = [];
23321
- const seen = /* @__PURE__ */ new Set();
23322
- const add = (finding) => {
23323
- if (seen.has(finding.pattern)) return;
23324
- seen.add(finding.pattern);
23325
- findings.push(finding);
23326
- };
23327
- for (const group of detectJunk(presentPaths, rules)) {
23328
- add({
23329
- source: "present",
23330
- severity: "imminent",
23331
- label: group.rule.label,
23332
- pattern: group.rule.pattern,
23333
- matchedPaths: group.paths
23334
- });
23335
- }
23336
- for (const rule of proposalRules) {
23337
- if (!presentMarkers.has(rule.marker)) continue;
23338
- for (const pattern of rule.patterns) {
23339
- add({
23340
- source: "marker",
23341
- severity: "predicted",
23342
- label: `${rule.marker} \u2192 ${pattern}`,
23343
- pattern,
23344
- matchedPaths: []
23345
- });
23346
- }
23347
- }
23348
- for (const pattern of gitignorePatterns) {
23349
- const trimmed = pattern.trim();
23350
- if (!trimmed) continue;
23351
- add({
23352
- source: "gitignore",
23353
- severity: "predicted",
23354
- label: trimmed,
23355
- pattern: trimmed,
23356
- matchedPaths: []
23357
- });
23358
- }
23359
- const maxSeverity = findings.some(
23360
- (finding) => finding.severity === "imminent"
23361
- ) ? "imminent" : findings.length > 0 ? "predicted" : null;
23362
- return {
23363
- findings,
23364
- proposedPatterns: findings.map((finding) => finding.pattern),
23365
- maxSeverity
23366
- };
23367
- };
23368
- var resolveIgnorePatterns = ({
23369
- bakedIn,
23370
- storeSetting,
23371
- localOverride
23372
- }) => {
23373
- const seen = /* @__PURE__ */ new Set();
23374
- const resolved = [];
23375
- for (const pattern of [...bakedIn, ...storeSetting, ...localOverride]) {
23376
- const trimmed = pattern.trim();
23377
- if (!trimmed || seen.has(trimmed)) continue;
23378
- seen.add(trimmed);
23379
- resolved.push(trimmed);
23380
- }
23381
- return resolved;
23382
- };
23383
- var parseLocalIgnoreFile = (text) => {
23384
- const patterns = [];
23385
- for (const rawLine of text.split(/\r?\n/)) {
23386
- const line = rawLine.trim();
23387
- if (!line || line.startsWith("#")) continue;
23388
- patterns.push(line);
23389
- }
23390
- return patterns;
23391
- };
23392
-
23393
23588
  // ../../packages/store-tree/src/patterns.ts
23394
23589
  var escapeRegex2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23395
- var basename2 = (path20) => {
23396
- const segments2 = path20.split("/").filter(Boolean);
23590
+ var basename2 = (path22) => {
23591
+ const segments2 = path22.split("/").filter(Boolean);
23397
23592
  return segments2[segments2.length - 1] ?? "";
23398
23593
  };
23399
23594
  var normalizeStorePathPattern = (pattern) => pattern.trim();
@@ -23411,7 +23606,7 @@ var storePathPatternToRegex = (pattern) => {
23411
23606
  return new RegExp(source, "i");
23412
23607
  };
23413
23608
  var doesStorePathPatternMatch = ({
23414
- path: path20,
23609
+ path: path22,
23415
23610
  pattern,
23416
23611
  mode = "path"
23417
23612
  }) => {
@@ -23419,7 +23614,7 @@ var doesStorePathPatternMatch = ({
23419
23614
  if (!normalizedPattern) {
23420
23615
  return true;
23421
23616
  }
23422
- const normalizedPath = normalizeStorePath(path20);
23617
+ const normalizedPath = normalizeStorePath(path22);
23423
23618
  const target = mode === "basename" ? basename2(normalizedPath) : normalizedPath;
23424
23619
  if (patternHasWildcard(normalizedPattern)) {
23425
23620
  return storePathPatternToRegex(normalizedPattern).test(target);
@@ -23507,8 +23702,8 @@ var isIssueRecord = (value) => {
23507
23702
  return isRecord2(value) && typeof value.message === "string";
23508
23703
  };
23509
23704
  var formatIssue = (issue2) => {
23510
- const path20 = Array.isArray(issue2.path) && issue2.path.length > 0 ? issue2.path.map(String).join(".") : "";
23511
- 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;
23512
23707
  };
23513
23708
  var formatIssueArray = (issues) => {
23514
23709
  if (!issues.every(isIssueRecord)) {
@@ -23981,7 +24176,7 @@ var buildContentChangedConflictExplainer = (issues) => {
23981
24176
  body: [
23982
24177
  pathSubject(
23983
24178
  paths,
23984
- (path20) => `${path20} changed both locally and remotely.`,
24179
+ (path22) => `${path22} changed both locally and remotely.`,
23985
24180
  (count) => `${count} files changed both locally and remotely.`
23986
24181
  ),
23987
24182
  "Forum left these files alone because either overwrite direction could lose work."
@@ -24013,7 +24208,7 @@ var buildLocalDeleteRemoteChangeExplainer = (issues) => {
24013
24208
  body: [
24014
24209
  pathSubject(
24015
24210
  paths,
24016
- (path20) => `${path20} was deleted locally, but the remote file changed.`,
24211
+ (path22) => `${path22} was deleted locally, but the remote file changed.`,
24017
24212
  (count) => `${count} files were deleted locally, but changed remotely.`
24018
24213
  ),
24019
24214
  "Forum did not delete newer remote work automatically."
@@ -24045,7 +24240,7 @@ var buildMoveDestinationConflictExplainer = (issues) => {
24045
24240
  body: [
24046
24241
  pathSubject(
24047
24242
  paths,
24048
- (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.`,
24049
24244
  (count) => `${count} paths are remote move destinations that already exist locally.`
24050
24245
  ),
24051
24246
  "Forum synced unrelated files but left these paths alone."
@@ -24080,7 +24275,7 @@ var buildMoveRemoteChangeConflictExplainer = (issues) => {
24080
24275
  body: [
24081
24276
  pathSubject(
24082
24277
  paths,
24083
- (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.`,
24084
24279
  (count) => `${count} files were involved in moves while the other side changed them.`
24085
24280
  ),
24086
24281
  "Forum did not combine the move and content change automatically."
@@ -24112,7 +24307,7 @@ var buildMoveMoveConflictExplainer = (issues) => {
24112
24307
  body: [
24113
24308
  pathSubject(
24114
24309
  paths,
24115
- (path20) => `${path20} moved locally and remotely to different paths.`,
24310
+ (path22) => `${path22} moved locally and remotely to different paths.`,
24116
24311
  (count) => `${count} files moved locally and remotely to different paths.`
24117
24312
  ),
24118
24313
  "Forum left these moves unresolved because choosing a path automatically could hide work."
@@ -24144,7 +24339,7 @@ var buildDeleteMoveConflictExplainer = (issues) => {
24144
24339
  body: [
24145
24340
  pathSubject(
24146
24341
  paths,
24147
- (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.`,
24148
24343
  (count) => `${count} files were deleted on one side and moved on the other.`
24149
24344
  ),
24150
24345
  "Forum did not choose between delete and move automatically."
@@ -24176,7 +24371,7 @@ var buildReferenceLocalFileConflictExplainer = (issues) => {
24176
24371
  body: [
24177
24372
  pathSubject(
24178
24373
  paths,
24179
- (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.`,
24180
24375
  (count) => `${count} paths are now remote references, but local files have changes.`
24181
24376
  ),
24182
24377
  "Forum left the local files alone because replacing them would discard local work."
@@ -24208,7 +24403,7 @@ var buildUnsupportedLocalEntryExplainer = (issues) => {
24208
24403
  body: [
24209
24404
  pathSubject(
24210
24405
  paths,
24211
- (path20) => `${path20} is an unsupported local filesystem entry.`,
24406
+ (path22) => `${path22} is an unsupported local filesystem entry.`,
24212
24407
  (count) => `${count} local filesystem entries are unsupported.`
24213
24408
  ),
24214
24409
  "Forum skipped these paths and continued with unrelated files where it could."
@@ -24240,7 +24435,7 @@ var buildReadOnlyLocalChangeExplainer = (issues) => {
24240
24435
  body: [
24241
24436
  pathSubject(
24242
24437
  paths,
24243
- (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.`,
24244
24439
  (count) => `${count} files have local changes, but their sync rules are read-only.`
24245
24440
  ),
24246
24441
  "Forum left these files alone so remote updates cannot overwrite local work."
@@ -24337,7 +24532,7 @@ var checkoutConflictIssuesFromResult = (result) => checkoutIssuesFromResult(resu
24337
24532
  (issue2) => issue2.type === "conflict"
24338
24533
  );
24339
24534
  var formatCheckoutExplainer = (explainer) => {
24340
- 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}`)];
24341
24536
  const actionLines = explainer.actions.map(
24342
24537
  (action) => `${styleText.bold(`${action.label}:`)} ${action.text}`
24343
24538
  );
@@ -24361,7 +24556,7 @@ var formatCheckoutExplainers = (result) => {
24361
24556
 
24362
24557
  // src/gitIntegration.ts
24363
24558
  import { execFile as execFile2 } from "child_process";
24364
- import path from "path";
24559
+ import path4 from "path";
24365
24560
  import { promisify as promisify2 } from "util";
24366
24561
  var execFileAsync2 = promisify2(execFile2);
24367
24562
  var gitErrorMessage = (error51) => {
@@ -24386,7 +24581,7 @@ var runGit = async (cwd, args) => {
24386
24581
  };
24387
24582
  var resolveGitRoot = async (cwd) => {
24388
24583
  const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
24389
- return path.resolve(cwd, result.stdout.trim());
24584
+ return path4.resolve(cwd, result.stdout.trim());
24390
24585
  };
24391
24586
  var readGitStatusEntries = async (gitRoot) => {
24392
24587
  const result = await runGit(gitRoot, [
@@ -24466,59 +24661,80 @@ var commitGitChanges = async (gitRoot, message) => {
24466
24661
  };
24467
24662
 
24468
24663
  // src/checkoutNaming.ts
24469
- import { existsSync } from "fs";
24470
- import path2 from "path";
24471
- var projectConfigFilenames = [
24472
- "forum.yaml",
24473
- "forum.yml",
24474
- "doctrine.yaml",
24475
- "doctrine.yml"
24476
- ];
24477
- var defaultProjectConfigFilename = "forum.yaml";
24478
- var checkoutStateDirectoryNames = [".forum", ".doctrine"];
24664
+ var defaultProjectConfigFilename = ".forum/sync.yaml";
24665
+ var projectConfigFilenames = [defaultProjectConfigFilename];
24666
+ var checkoutStateDirectoryNames = [".forum"];
24479
24667
  var defaultCheckoutStateDirectoryName = ".forum";
24480
- var resolveCheckoutStateDirectoryName = (rootDir) => {
24481
- for (const name of checkoutStateDirectoryNames) {
24482
- if (existsSync(path2.join(rootDir, name))) {
24483
- 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
+ }
24484
24702
  }
24485
24703
  }
24486
- return defaultCheckoutStateDirectoryName;
24704
+ structuralPreflight.blockedDetails = [];
24705
+ return details;
24487
24706
  };
24488
- var isCheckoutStateDirectoryPath = (relativePath) => checkoutStateDirectoryNames.some(
24489
- (name) => relativePath === name || relativePath.startsWith(`${name}/`)
24490
- );
24491
24707
 
24492
24708
  // src/checkout.ts
24493
24709
  import { createHash as createHash2 } from "crypto";
24494
- import path7 from "path";
24710
+ import path9 from "path";
24495
24711
  import {
24496
24712
  lstat,
24497
- mkdir as mkdir3,
24713
+ mkdir as mkdir4,
24498
24714
  readdir as readdir2,
24499
- readFile as readFile2,
24715
+ readFile as readFile3,
24500
24716
  rename as rename2,
24501
- rm as rm2,
24717
+ rm as rm3,
24502
24718
  stat,
24503
24719
  writeFile as writeFile3
24504
24720
  } from "fs/promises";
24505
24721
  import { TextDecoder as TextDecoder2 } from "util";
24506
- import ignore from "ignore";
24722
+ import ignore2 from "ignore";
24507
24723
 
24508
24724
  // src/buildCheckoutFileManifestEntry.ts
24509
- var toManifestRemotePath = (path20) => path20.startsWith("/") ? path20 : `/${path20}`;
24725
+ var toManifestRemotePath = (path22) => path22.startsWith("/") ? path22 : `/${path22}`;
24510
24726
  var buildCheckoutFileManifestEntry = ({
24511
24727
  storageClass,
24512
24728
  hash: hash2,
24513
24729
  manifestEntry,
24514
- path: path20,
24730
+ path: path22,
24515
24731
  remoteFile,
24516
24732
  source,
24517
24733
  syncedAt
24518
24734
  }) => {
24519
24735
  const remoteFileId = remoteFile.id ?? remoteFile._id ?? manifestEntry?.remoteFileId;
24520
24736
  if (!remoteFileId) {
24521
- 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`);
24522
24738
  }
24523
24739
  return {
24524
24740
  storageClass,
@@ -24526,7 +24742,7 @@ var buildCheckoutFileManifestEntry = ({
24526
24742
  isExecutable: storageClass === "text" ? remoteFile.isExecutable === true || void 0 : void 0,
24527
24743
  lastSyncedAt: syncedAt,
24528
24744
  remoteFileId,
24529
- remotePath: remoteFile.path ?? manifestEntry?.remotePath ?? toManifestRemotePath(path20),
24745
+ remotePath: remoteFile.path ?? manifestEntry?.remotePath ?? toManifestRemotePath(path22),
24530
24746
  remoteUpdatedAt: remoteFile.updatedAt ?? null,
24531
24747
  remoteRevisionNumber: remoteFile.revisionNumber ?? null,
24532
24748
  ...source ? { source } : {}
@@ -24628,14 +24844,14 @@ var buildSyncCheckoutSnapshot = ({
24628
24844
  };
24629
24845
 
24630
24846
  // src/checkoutDirectories.ts
24631
- import path3 from "path";
24632
- import { mkdir, readdir, rmdir } from "fs/promises";
24847
+ import path6 from "path";
24848
+ import { mkdir as mkdir2, readdir, rmdir } from "fs/promises";
24633
24849
  var parentDirectoriesForPath = (relativePath) => {
24634
24850
  const directories = [];
24635
- let directory = path3.posix.dirname(relativePath);
24851
+ let directory = path6.posix.dirname(relativePath);
24636
24852
  while (directory && directory !== ".") {
24637
24853
  directories.push(directory);
24638
- directory = path3.posix.dirname(directory);
24854
+ directory = path6.posix.dirname(directory);
24639
24855
  }
24640
24856
  return directories;
24641
24857
  };
@@ -24660,7 +24876,7 @@ var prepareCheckoutDirectories = async ({
24660
24876
  const directories = checkoutParentDirectories(relativePaths);
24661
24877
  await Promise.all(
24662
24878
  directories.map(
24663
- (directory) => mkdir(path3.join(localPath, directory), { recursive: true })
24879
+ (directory) => mkdir2(path6.join(localPath, directory), { recursive: true })
24664
24880
  )
24665
24881
  );
24666
24882
  return directories;
@@ -24674,7 +24890,7 @@ var cleanupCheckoutDirectories = async ({
24674
24890
  if (checkoutStateDirectoryNames.includes(directory)) {
24675
24891
  continue;
24676
24892
  }
24677
- const absolutePath = path3.join(localPath, directory);
24893
+ const absolutePath = path6.join(localPath, directory);
24678
24894
  try {
24679
24895
  const entries = await readdir(absolutePath);
24680
24896
  if (entries.length > 0) {
@@ -24697,13 +24913,13 @@ var cleanupCheckoutDirectories = async ({
24697
24913
  };
24698
24914
 
24699
24915
  // src/planRemoteDirectoryCleanup.ts
24700
- import path4 from "path";
24916
+ import path7 from "path";
24701
24917
  var ancestorDirectories = (relativePath) => {
24702
24918
  const directories = [];
24703
- let directory = path4.posix.dirname(relativePath);
24919
+ let directory = path7.posix.dirname(relativePath);
24704
24920
  while (directory && directory !== ".") {
24705
24921
  directories.push(directory);
24706
- directory = path4.posix.dirname(directory);
24922
+ directory = path7.posix.dirname(directory);
24707
24923
  }
24708
24924
  return directories;
24709
24925
  };
@@ -24720,7 +24936,7 @@ var planRemoteDirectoryCleanup = (removedPaths) => {
24720
24936
  };
24721
24937
 
24722
24938
  // src/cleanupRemoteCheckoutDirectories.ts
24723
- var addLeadingSlash = (relativePath) => relativePath ? `/${relativePath}` : "/";
24939
+ var addLeadingSlash2 = (relativePath) => relativePath ? `/${relativePath}` : "/";
24724
24940
  var cleanupRemoteCheckoutDirectories = async ({
24725
24941
  client,
24726
24942
  removedPaths,
@@ -24728,7 +24944,7 @@ var cleanupRemoteCheckoutDirectories = async ({
24728
24944
  }) => {
24729
24945
  const removed = [];
24730
24946
  for (const directory of planRemoteDirectoryCleanup(removedPaths)) {
24731
- const directoryPath = addLeadingSlash(directory);
24947
+ const directoryPath = addLeadingSlash2(directory);
24732
24948
  try {
24733
24949
  const listing = await client.listDirectoryByRef({
24734
24950
  ...storeRef,
@@ -24751,53 +24967,6 @@ var cleanupRemoteCheckoutDirectories = async ({
24751
24967
  return removed;
24752
24968
  };
24753
24969
 
24754
- // src/mapSyncRulePath.ts
24755
- var assertSafeRelativePath = (relativePath) => {
24756
- const segments2 = relativePath.split("/");
24757
- if (relativePath.startsWith("/") || segments2.some((segment) => segment === "." || segment === "..")) {
24758
- throw new Error(`Unsafe checkout path: ${relativePath}`);
24759
- }
24760
- };
24761
- var toRelativePath = (storePath) => {
24762
- const relativePath = storePath.replace(/^\/+/, "");
24763
- assertSafeRelativePath(relativePath);
24764
- return relativePath;
24765
- };
24766
- var joinRelativePath = (...parts) => parts.flatMap((part) => part.split("/")).filter(Boolean).join("/");
24767
- var addLeadingSlash2 = (relativePath) => relativePath ? `/${relativePath}` : "/";
24768
- var stripSyncRuleTarget = (localRelativePath, rule) => {
24769
- if (!rule.target) {
24770
- return localRelativePath;
24771
- }
24772
- if (localRelativePath === rule.target) {
24773
- return "";
24774
- }
24775
- if (!localRelativePath.startsWith(`${rule.target}/`)) {
24776
- return null;
24777
- }
24778
- return localRelativePath.slice(rule.target.length + 1);
24779
- };
24780
- var mapLocalPathToSyncRuleRemotePath = (localStorePath, rule) => {
24781
- const localRelativePath = toRelativePath(localStorePath);
24782
- const ruleRelativePath = stripSyncRuleTarget(localRelativePath, rule);
24783
- if (ruleRelativePath === null) {
24784
- throw new Error(
24785
- `Path is outside sync rule target ${rule.target || "."}: ${localRelativePath}`
24786
- );
24787
- }
24788
- return addLeadingSlash2(joinRelativePath(rule.path, ruleRelativePath));
24789
- };
24790
- var mapRemotePathToSyncRuleLocalPath = (remoteStorePath, rule) => {
24791
- const remoteRelativePath = toRelativePath(remoteStorePath);
24792
- if (rule.path && remoteRelativePath !== rule.path && !remoteRelativePath.startsWith(`${rule.path}/`)) {
24793
- throw new Error(
24794
- `Remote path is outside sync rule path ${rule.path}: ${remoteRelativePath}`
24795
- );
24796
- }
24797
- const ruleRelativePath = rule.path ? remoteRelativePath === rule.path ? "" : remoteRelativePath.slice(rule.path.length + 1) : remoteRelativePath;
24798
- return addLeadingSlash2(joinRelativePath(rule.target, ruleRelativePath));
24799
- };
24800
-
24801
24970
  // src/planSyncCheckoutPaths.ts
24802
24971
  var detailRelatedPaths = (detail) => [
24803
24972
  detail.relativePath,
@@ -24971,6 +25140,8 @@ var planRemoteRenameBeforePull = ({
24971
25140
 
24972
25141
  // src/planSyncCheckoutFile.ts
24973
25142
  var planSyncCheckoutFile = ({
25143
+ conflictPolicy,
25144
+ resolvedConflict,
24974
25145
  force,
24975
25146
  localIsExecutable = false,
24976
25147
  localHash,
@@ -24983,6 +25154,34 @@ var planSyncCheckoutFile = ({
24983
25154
  }) => {
24984
25155
  const hasLocal = localHash !== null;
24985
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
+ }
24986
25185
  if (remoteKind === "reference") {
24987
25186
  if (!force && (hasManifest || hasLocal)) {
24988
25187
  return {
@@ -25038,7 +25237,6 @@ var planSyncCheckoutFile = ({
25038
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"
25039
25238
  };
25040
25239
  }
25041
- const localChanged = localHash !== manifestHash || localIsExecutable !== manifestIsExecutable;
25042
25240
  if (!force && localChanged && remoteVersionChanged) {
25043
25241
  return {
25044
25242
  type: "conflict",
@@ -25081,19 +25279,9 @@ var selectCheckoutFilePathsForSyncRule = ({
25081
25279
 
25082
25280
  // src/projectConfig.ts
25083
25281
  import { createHash } from "crypto";
25084
- import path5 from "path";
25085
- 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";
25086
25284
  import { parse as parse3, stringify } from "yaml";
25087
- var warnedLegacyIgnorePaths = /* @__PURE__ */ new Set();
25088
- var warnLegacyIgnoreField = (filePath) => {
25089
- if (warnedLegacyIgnorePaths.has(filePath)) {
25090
- return;
25091
- }
25092
- warnedLegacyIgnorePaths.add(filePath);
25093
- renderWarning(
25094
- `ignore in ${filePath} is no longer used; move these to the store with \`fm ignore add\`, or to .forum/ignore`
25095
- );
25096
- };
25097
25285
  var fileExists = async (filePath) => {
25098
25286
  try {
25099
25287
  await access(filePath);
@@ -25126,7 +25314,9 @@ var parseProjectConfig = (filePath, source) => {
25126
25314
  );
25127
25315
  }
25128
25316
  if (ignore3 !== void 0) {
25129
- 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
+ );
25130
25320
  }
25131
25321
  return {
25132
25322
  ...defaultStore ? { defaultStore } : {},
@@ -25207,7 +25397,7 @@ var normalizeSyncPath = ({
25207
25397
  value
25208
25398
  }) => {
25209
25399
  const trimmed = value.trim();
25210
- if (path5.isAbsolute(trimmed)) {
25400
+ if (path8.isAbsolute(trimmed)) {
25211
25401
  throw new Error(
25212
25402
  `Invalid Forum config at ${filePath}: ${fieldName} must be a relative path.`
25213
25403
  );
@@ -25360,16 +25550,16 @@ var parseQualifiedConfigStore = (filePath, fieldName, store) => {
25360
25550
  return `${segments2[0]}/${segments2[1]}`;
25361
25551
  };
25362
25552
  var findForumProjectConfig = async (startDir = process.cwd()) => {
25363
- let current = path5.resolve(startDir);
25553
+ let current = path8.resolve(startDir);
25364
25554
  while (true) {
25365
25555
  for (const filename of projectConfigFilenames) {
25366
- const candidate = path5.join(current, filename);
25556
+ const candidate = path8.join(current, filename);
25367
25557
  if (await fileExists(candidate)) {
25368
- const source = await readFile(candidate, "utf8");
25558
+ const source = await readFile2(candidate, "utf8");
25369
25559
  return parseProjectConfig(candidate, source);
25370
25560
  }
25371
25561
  }
25372
- const parent = path5.dirname(current);
25562
+ const parent = path8.dirname(current);
25373
25563
  if (parent === current) {
25374
25564
  return null;
25375
25565
  }
@@ -25404,28 +25594,8 @@ var writeForumProjectConfig = async (filePath, config2) => {
25404
25594
  target: rule.target || "."
25405
25595
  }));
25406
25596
  }
25407
- await writeFile(filePath, stringify(value), "utf8");
25408
- };
25409
-
25410
- // src/atomicWriteTextFile.ts
25411
- import path6 from "path";
25412
- import { chmod, mkdir as mkdir2, rename, rm, writeFile as writeFile2 } from "fs/promises";
25413
- var atomicWriteTextFile = async (filePath, text, isExecutable = false) => {
25414
- const parentDir = path6.dirname(filePath);
25415
- const tempPath = path6.join(
25416
- parentDir,
25417
- `.${path6.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
25418
- );
25419
- await mkdir2(parentDir, { recursive: true });
25420
- try {
25421
- await writeFile2(tempPath, text, "utf8");
25422
- await chmod(tempPath, isExecutable ? 493 : 420);
25423
- await rename(tempPath, filePath);
25424
- } catch (error51) {
25425
- await rm(tempPath, { force: true }).catch(() => {
25426
- });
25427
- throw error51;
25428
- }
25597
+ await mkdir3(path8.dirname(filePath), { recursive: true });
25598
+ await writeFile2(filePath, stringify(value), "utf8");
25429
25599
  };
25430
25600
 
25431
25601
  // src/markdownStoreLinks.ts
@@ -25500,10 +25670,8 @@ var rewriteRawUrlsToStorePathLinks = ({
25500
25670
  };
25501
25671
 
25502
25672
  // src/checkout.ts
25503
- var reservedRootFiles = new Set(projectConfigFilenames);
25504
- var reservedRootFileNames = [...reservedRootFiles];
25505
25673
  var checkoutFilesManifestFilename = "files.json";
25506
- var localIgnoreFilename = "ignore";
25674
+ var localIgnoreFilename = "usr_ignore";
25507
25675
  var gitDirectoryName = ".git";
25508
25676
  var defaultConcurrency = 8;
25509
25677
  var utf8Decoder = new TextDecoder2("utf-8", { fatal: true });
@@ -25672,18 +25840,14 @@ var rewriteMarkdownForRemoteStore = ({
25672
25840
  targets: markdownLinkTargetsFromManifest(manifest),
25673
25841
  text
25674
25842
  }) : text;
25675
- var isReservedCheckoutFile = (relativePath) => {
25676
- const filename = relativePath.split("/").at(-1) ?? relativePath;
25677
- return reservedRootFiles.has(filename);
25678
- };
25679
- var isReservedCheckoutMetadataPath = (relativePath) => isReservedCheckoutFile(relativePath) || isCheckoutStateDirectoryPath(relativePath) || // `.git/` is the checkout's own VCS metadata — structurally never content,
25843
+ var isReservedCheckoutMetadataPath = (relativePath) => isCheckoutStateDirectoryPath(relativePath) || // `.git/` is the checkout's own VCS metadata — structurally never content,
25680
25844
  // like `.forum/`. It is reserved (not an ignore *rule*): there are no
25681
25845
  // baked-in ignore patterns, but tooling directories are still never synced.
25682
25846
  relativePath === gitDirectoryName || relativePath.startsWith(`${gitDirectoryName}/`);
25683
25847
  var reservedCheckoutMetadataMessage = "reserved checkout metadata path; ignored by local sync";
25684
25848
  var assertSafeRelativePath2 = (relativePath) => {
25685
25849
  const segments2 = relativePath.split("/");
25686
- if (path7.isAbsolute(relativePath) || segments2.some((segment) => segment === "." || segment === "..")) {
25850
+ if (path9.isAbsolute(relativePath) || segments2.some((segment) => segment === "." || segment === "..")) {
25687
25851
  throw new Error(`Unsafe checkout path: ${relativePath}`);
25688
25852
  }
25689
25853
  };
@@ -25997,8 +26161,8 @@ var createSyncRuleClient = (client, rule) => {
25997
26161
  }
25998
26162
  };
25999
26163
  };
26000
- var syncRuleFileParentPath = (rule) => path7.posix.dirname(addLeadingSlash3(rule.target));
26001
- 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));
26002
26166
  var assertSyncRuleFilePath = (storePath, rule) => {
26003
26167
  const relativePath = toRelativePath2(storePath);
26004
26168
  if (relativePath !== rule.target) {
@@ -26100,7 +26264,7 @@ var classifySyncRuleRemoteRoot = async (client, storeRef, rule) => {
26100
26264
  const listing = await client.listDirectoryByRef({
26101
26265
  ...storeRef,
26102
26266
  includeHidden: rule.includeHidden,
26103
- directoryPath: path7.posix.dirname(remotePath)
26267
+ directoryPath: path9.posix.dirname(remotePath)
26104
26268
  });
26105
26269
  const entry = listing.entries.find(
26106
26270
  (candidate) => candidate.path === remotePath
@@ -26124,7 +26288,7 @@ var classifySyncRuleLocalTarget = async (rootDir, rule) => {
26124
26288
  return "directory";
26125
26289
  }
26126
26290
  try {
26127
- const details = await lstat(path7.join(rootDir, rule.target));
26291
+ const details = await lstat(path9.join(rootDir, rule.target));
26128
26292
  if (details.isFile()) {
26129
26293
  return "file";
26130
26294
  }
@@ -26246,34 +26410,32 @@ var normalizeCheckoutIncludePaths = (includePaths) => {
26246
26410
  }
26247
26411
  return normalized;
26248
26412
  };
26249
- var checkoutFilesManifestPath = (rootDir) => path7.join(
26413
+ var checkoutFilesManifestPath = (rootDir) => path9.join(
26250
26414
  rootDir,
26251
- resolveCheckoutStateDirectoryName(rootDir),
26415
+ resolveCheckoutStateDirectoryName(),
26252
26416
  checkoutFilesManifestFilename
26253
26417
  );
26254
26418
  var hashBytes = (bytes) => `sha256:${createHash2("sha256").update(bytes).digest("hex")}`;
26255
26419
  var hashText = (text) => hashBytes(Buffer.from(text, "utf8"));
26256
26420
  var hashLocalFile = async (filePath) => {
26257
- return hashBytes(await readFile2(filePath));
26421
+ return hashBytes(await readFile3(filePath));
26258
26422
  };
26259
26423
  var isExecutableMode = (mode) => (mode & 73) !== 0;
26260
26424
  var emptyCheckoutFilesManifest = () => ({
26261
26425
  files: {},
26262
26426
  references: {},
26263
26427
  syncRules: {},
26264
- ignorePatterns: [],
26265
26428
  ignoreOnboardedAt: null,
26266
26429
  version: 1
26267
26430
  });
26268
26431
  var readCheckoutFilesManifest = async (rootDir) => {
26269
26432
  try {
26270
- const raw = await readFile2(checkoutFilesManifestPath(rootDir), "utf8");
26433
+ const raw = await readFile3(checkoutFilesManifestPath(rootDir), "utf8");
26271
26434
  const parsed = JSON.parse(raw);
26272
26435
  return {
26273
26436
  files: parsed.files ?? {},
26274
26437
  references: parsed.references ?? {},
26275
26438
  syncRules: parsed.syncRules ?? {},
26276
- ignorePatterns: parsed.ignorePatterns ?? [],
26277
26439
  ignoreOnboardedAt: parsed.ignoreOnboardedAt ?? null,
26278
26440
  version: 1
26279
26441
  };
@@ -26286,12 +26448,12 @@ var readCheckoutFilesManifest = async (rootDir) => {
26286
26448
  };
26287
26449
  var writeCheckoutFilesManifest = async (rootDir, manifest) => {
26288
26450
  const filePath = checkoutFilesManifestPath(rootDir);
26289
- const parentDir = path7.dirname(filePath);
26290
- const tempPath = path7.join(
26451
+ const parentDir = path9.dirname(filePath);
26452
+ const tempPath = path9.join(
26291
26453
  parentDir,
26292
26454
  `.${checkoutFilesManifestFilename}.${process.pid}.${Date.now()}.tmp`
26293
26455
  );
26294
- await mkdir3(parentDir, { recursive: true });
26456
+ await mkdir4(parentDir, { recursive: true });
26295
26457
  try {
26296
26458
  await writeFile3(
26297
26459
  tempPath,
@@ -26312,9 +26474,6 @@ var writeCheckoutFilesManifest = async (rootDir, manifest) => {
26312
26474
  ([leftId], [rightId]) => leftId < rightId ? -1 : leftId > rightId ? 1 : 0
26313
26475
  )
26314
26476
  ),
26315
- ignorePatterns: [...manifest.ignorePatterns].sort(
26316
- (left, right) => left < right ? -1 : left > right ? 1 : 0
26317
- ),
26318
26477
  ignoreOnboardedAt: manifest.ignoreOnboardedAt,
26319
26478
  version: 1
26320
26479
  },
@@ -26326,7 +26485,7 @@ var writeCheckoutFilesManifest = async (rootDir, manifest) => {
26326
26485
  );
26327
26486
  await rename2(tempPath, filePath);
26328
26487
  } catch (error51) {
26329
- await rm2(tempPath, { force: true }).catch(() => {
26488
+ await rm3(tempPath, { force: true }).catch(() => {
26330
26489
  });
26331
26490
  throw error51;
26332
26491
  }
@@ -26377,7 +26536,7 @@ var readFileSnapshot = async (filePath) => {
26377
26536
  };
26378
26537
  var isFileSnapshotEqual = (left, right) => left.size === right.size && left.mtimeMs === right.mtimeMs;
26379
26538
  var createIgnoreMatcher = (patterns) => {
26380
- const matcher = ignore();
26539
+ const matcher = ignore2();
26381
26540
  matcher.add(patterns);
26382
26541
  return matcher;
26383
26542
  };
@@ -26431,7 +26590,7 @@ var deleteLocalFileIfPresent = async (filePath) => {
26431
26590
  if (!details.isFile()) {
26432
26591
  return false;
26433
26592
  }
26434
- await rm2(filePath, { force: true });
26593
+ await rm3(filePath, { force: true });
26435
26594
  return true;
26436
26595
  } catch (error51) {
26437
26596
  if (isFileNotFoundError(error51)) {
@@ -26442,7 +26601,7 @@ var deleteLocalFileIfPresent = async (filePath) => {
26442
26601
  };
26443
26602
  var moveLocalFileIfPresent = async (sourcePath, destinationPath) => {
26444
26603
  try {
26445
- await mkdir3(path7.dirname(destinationPath), { recursive: true });
26604
+ await mkdir4(path9.dirname(destinationPath), { recursive: true });
26446
26605
  await rename2(sourcePath, destinationPath);
26447
26606
  return true;
26448
26607
  } catch (error51) {
@@ -26454,7 +26613,7 @@ var moveLocalFileIfPresent = async (sourcePath, destinationPath) => {
26454
26613
  };
26455
26614
  var readLocalFileHashIfPresent = async (rootDir, relativePath) => {
26456
26615
  try {
26457
- return await hashLocalFile(path7.join(rootDir, relativePath));
26616
+ return await hashLocalFile(path9.join(rootDir, relativePath));
26458
26617
  } catch (error51) {
26459
26618
  if (isFileNotFoundError(error51)) {
26460
26619
  return null;
@@ -26513,7 +26672,7 @@ var reconcileTrackedRemoteDelete = async ({
26513
26672
  return localFileHash !== null ? "deleted" : "ignored";
26514
26673
  }
26515
26674
  const deletedLocalFile = await deleteLocalFileIfPresent(
26516
- path7.join(localPath, relativePath)
26675
+ path9.join(localPath, relativePath)
26517
26676
  );
26518
26677
  delete manifest.files[relativePath];
26519
26678
  return deletedLocalFile ? "deleted" : "ignored";
@@ -26593,20 +26752,20 @@ var readOnlyCheckoutChangeResults = (changes) => changes.map((change) => ({
26593
26752
  status: "failed"
26594
26753
  }));
26595
26754
  var resetCheckoutSyncState = async (rootDir) => {
26596
- await rm2(checkoutFilesManifestPath(rootDir), { force: true });
26755
+ await rm3(checkoutFilesManifestPath(rootDir), { force: true });
26597
26756
  };
26598
26757
  var atomicWriteBinaryFile = async (filePath, bytes) => {
26599
- const parentDir = path7.dirname(filePath);
26600
- const tempPath = path7.join(
26758
+ const parentDir = path9.dirname(filePath);
26759
+ const tempPath = path9.join(
26601
26760
  parentDir,
26602
- `.${path7.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
26761
+ `.${path9.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
26603
26762
  );
26604
- await mkdir3(parentDir, { recursive: true });
26763
+ await mkdir4(parentDir, { recursive: true });
26605
26764
  try {
26606
26765
  await writeFile3(tempPath, bytes);
26607
26766
  await rename2(tempPath, filePath);
26608
26767
  } catch (error51) {
26609
- await rm2(tempPath, { force: true }).catch(() => {
26768
+ await rm3(tempPath, { force: true }).catch(() => {
26610
26769
  });
26611
26770
  throw error51;
26612
26771
  }
@@ -26644,7 +26803,7 @@ var readStableCheckoutFile = async (absolutePath, relativePath, attempts = 3) =>
26644
26803
  for (let attempt = 0; attempt < attempts; attempt += 1) {
26645
26804
  try {
26646
26805
  const before = await readFileSnapshot(absolutePath);
26647
- const buffer = await readFile2(absolutePath);
26806
+ const buffer = await readFile3(absolutePath);
26648
26807
  const after = await readFileSnapshot(absolutePath);
26649
26808
  if (!isFileSnapshotEqual(before, after)) {
26650
26809
  sawUnstableRead = true;
@@ -26704,8 +26863,8 @@ var readStableCheckoutFile = async (absolutePath, relativePath, attempts = 3) =>
26704
26863
  var walkLocalCheckoutFiles = async (rootDir, directoryPath, includePaths, matcher, scan) => {
26705
26864
  const entries = await readdir2(directoryPath, { withFileTypes: true });
26706
26865
  for (const entry of entries) {
26707
- const absolutePath = path7.join(directoryPath, entry.name);
26708
- 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("/");
26709
26868
  const classification = classifyCheckoutPath(
26710
26869
  relativePath,
26711
26870
  entry.isDirectory(),
@@ -26803,7 +26962,7 @@ var readRemoteCheckoutEntry = async (client, storeRef, relativePath) => {
26803
26962
  const storePath = toStorePath2(relativePath);
26804
26963
  const listing = await client.listDirectoryByRef({
26805
26964
  ...storeRef,
26806
- directoryPath: path7.posix.dirname(storePath)
26965
+ directoryPath: path9.posix.dirname(storePath)
26807
26966
  });
26808
26967
  const entry = listing.entries.find(
26809
26968
  (candidate) => candidate.path === storePath
@@ -26817,7 +26976,7 @@ var readRemoteStoreEntry = async (client, storeRef, relativePath) => {
26817
26976
  const storePath = toStorePath2(relativePath);
26818
26977
  const listing = await client.listDirectoryByRef({
26819
26978
  ...storeRef,
26820
- directoryPath: path7.posix.dirname(storePath)
26979
+ directoryPath: path9.posix.dirname(storePath)
26821
26980
  });
26822
26981
  return listing.entries.find(
26823
26982
  (candidate) => candidate.path === storePath
@@ -26841,7 +27000,7 @@ var readReferenceSourceForPath = async (client, storeRef, relativePath) => {
26841
27000
  return void 0;
26842
27001
  };
26843
27002
  var readLocalCheckoutFile = async (rootDir, relativePath) => {
26844
- const absolutePath = path7.join(rootDir, relativePath);
27003
+ const absolutePath = path9.join(rootDir, relativePath);
26845
27004
  let details;
26846
27005
  try {
26847
27006
  details = await lstat(absolutePath);
@@ -27291,7 +27450,7 @@ var joinDirectRootPath = (rootPath, relativePath) => {
27291
27450
  if (!relativePath) {
27292
27451
  return rootPath;
27293
27452
  }
27294
- return path7.posix.join(rootPath, relativePath);
27453
+ return path9.posix.join(rootPath, relativePath);
27295
27454
  };
27296
27455
  var directStorePath = (relativePath) => relativePath ? toStorePath2(relativePath) : "/";
27297
27456
  var relativePathFromDirectRoot = (rootPath, storePath) => {
@@ -27300,7 +27459,7 @@ var relativePathFromDirectRoot = (rootPath, storePath) => {
27300
27459
  return relativePath;
27301
27460
  }
27302
27461
  if (relativePath === rootPath) {
27303
- return path7.posix.basename(rootPath);
27462
+ return path9.posix.basename(rootPath);
27304
27463
  }
27305
27464
  const prefix = `${rootPath}/`;
27306
27465
  if (!relativePath.startsWith(prefix)) {
@@ -27348,22 +27507,26 @@ var updateFileManifestEntry = ({
27348
27507
  hash: hash2,
27349
27508
  manifest,
27350
27509
  manifestEntry,
27351
- path: path20,
27510
+ path: path22,
27352
27511
  remoteFile,
27353
27512
  source,
27354
27513
  syncedAt
27355
27514
  }) => {
27356
- manifest.files[path20] = buildCheckoutFileManifestEntry({
27515
+ manifest.files[path22] = buildCheckoutFileManifestEntry({
27357
27516
  storageClass,
27358
27517
  hash: hash2,
27359
27518
  manifestEntry,
27360
- path: path20,
27519
+ path: path22,
27361
27520
  remoteFile,
27362
27521
  source,
27363
27522
  syncedAt
27364
27523
  });
27365
27524
  };
27366
27525
  var reconcileRemoteRenamesBeforePull = async ({
27526
+ client,
27527
+ conflictPolicy,
27528
+ localFiles,
27529
+ storeRef,
27367
27530
  dryRun,
27368
27531
  files,
27369
27532
  force,
@@ -27375,7 +27538,8 @@ var reconcileRemoteRenamesBeforePull = async ({
27375
27538
  for (const [relativePath, entry] of Object.entries(manifest.files)) {
27376
27539
  manifestPathByRemoteFileId.set(entry.remoteFileId, relativePath);
27377
27540
  }
27378
- for (const { entry, relativePath } of files) {
27541
+ for (const remoteFile of files) {
27542
+ const { entry, relativePath } = remoteFile;
27379
27543
  if (!entry.id) {
27380
27544
  continue;
27381
27545
  }
@@ -27383,18 +27547,40 @@ var reconcileRemoteRenamesBeforePull = async ({
27383
27547
  if (!manifestPath || manifestPath === relativePath) {
27384
27548
  continue;
27385
27549
  }
27386
- const sourcePath = path7.join(localPath, manifestPath);
27387
- const destinationPath = path7.join(localPath, relativePath);
27550
+ const sourcePath = path9.join(localPath, manifestPath);
27551
+ const destinationPath = path9.join(localPath, relativePath);
27388
27552
  const manifestEntry = manifest.files[manifestPath];
27389
27553
  const destinationExists = await pathExists(destinationPath);
27390
27554
  const localChanged = await hasTrackedLocalChange({
27391
27555
  localPath,
27392
27556
  manifestEntry,
27393
27557
  relativePath: manifestPath
27394
- });
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
+ }
27395
27581
  const plan = planRemoteRenameBeforePull({
27396
27582
  destinationExists,
27397
- force,
27583
+ force: force || conflictPolicy === "remote",
27398
27584
  localChanged
27399
27585
  });
27400
27586
  if (plan.type === "conflict") {
@@ -27421,6 +27607,33 @@ var reconcileRemoteRenamesBeforePull = async ({
27421
27607
  );
27422
27608
  continue;
27423
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
+ }
27424
27637
  if (!dryRun) {
27425
27638
  if (plan.shouldDeleteDestination) {
27426
27639
  await deleteLocalFileIfPresent(destinationPath);
@@ -27463,6 +27676,9 @@ var localRenameCandidateDestinations = (relativePath, manifest, localFilesByHash
27463
27676
  var localRenameCandidateSources = (file2, deletePaths, manifest) => deletePaths.filter((relativePath) => manifest.files[relativePath]?.hash === file2.hash).sort();
27464
27677
  var preflightLocalRenamesBeforePush = async ({
27465
27678
  client,
27679
+ conflictPolicy,
27680
+ remoteFiles,
27681
+ localPath,
27466
27682
  deletePaths,
27467
27683
  dryRun,
27468
27684
  localFiles,
@@ -27546,6 +27762,41 @@ var preflightLocalRenamesBeforePush = async ({
27546
27762
  if (!manifestEntry) {
27547
27763
  continue;
27548
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
+ }
27549
27800
  if (!dryRun) {
27550
27801
  await client.movePathByRef({
27551
27802
  ...storeRef,
@@ -27572,14 +27823,10 @@ var preflightLocalRenamesBeforePush = async ({
27572
27823
  remainingDeletePaths
27573
27824
  };
27574
27825
  };
27575
- var localIgnoreFilePath = (rootDir) => path7.join(
27576
- rootDir,
27577
- resolveCheckoutStateDirectoryName(rootDir),
27578
- localIgnoreFilename
27579
- );
27826
+ var localIgnoreFilePath = (rootDir) => path9.join(rootDir, resolveCheckoutStateDirectoryName(), localIgnoreFilename);
27580
27827
  var readLocalIgnorePatterns = async (rootDir) => {
27581
27828
  try {
27582
- const raw = await readFile2(localIgnoreFilePath(rootDir), "utf8");
27829
+ const raw = await readFile3(localIgnoreFilePath(rootDir), "utf8");
27583
27830
  return parseLocalIgnoreFile(raw);
27584
27831
  } catch (error51) {
27585
27832
  if (errorCode(error51) === "ENOENT") {
@@ -27588,20 +27835,6 @@ var readLocalIgnorePatterns = async (rootDir) => {
27588
27835
  throw error51;
27589
27836
  }
27590
27837
  };
27591
- var readStoreIgnoreCache = async (rootDir) => {
27592
- const manifest = await readCheckoutFilesManifest(rootDir);
27593
- return manifest.ignorePatterns;
27594
- };
27595
- var writeStoreIgnoreCache = async (rootDir, storeIgnorePatterns) => {
27596
- const manifest = await readCheckoutFilesManifest(rootDir);
27597
- const ignorePatterns = resolveIgnorePatterns({
27598
- bakedIn: [],
27599
- storeSetting: storeIgnorePatterns,
27600
- localOverride: []
27601
- });
27602
- await writeCheckoutFilesManifest(rootDir, { ...manifest, ignorePatterns });
27603
- return ignorePatterns;
27604
- };
27605
27838
  var readIgnoreOnboardedAt = async (rootDir) => {
27606
27839
  const manifest = await readCheckoutFilesManifest(rootDir);
27607
27840
  return manifest.ignoreOnboardedAt;
@@ -27617,22 +27850,29 @@ var resolveCheckoutConfig = async (startDir = process.cwd()) => {
27617
27850
  const config2 = await findForumProjectConfig(startDir);
27618
27851
  if (!config2) {
27619
27852
  throw new Error(
27620
- "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."
27621
27854
  );
27622
27855
  }
27623
- const rootDir = path7.dirname(config2.path);
27624
- const [cachedStoreIgnore, localIgnore] = await Promise.all([
27625
- readStoreIgnoreCache(rootDir),
27626
- readLocalIgnorePatterns(rootDir)
27627
- ]);
27628
- const ignore3 = resolveIgnorePatterns({
27629
- bakedIn: [],
27630
- storeSetting: cachedStoreIgnore,
27631
- localOverride: localIgnore
27632
- });
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;
27633
27871
  return {
27634
27872
  ...config2,
27873
+ ignoreMatchersByRule,
27635
27874
  ignore: ignore3,
27875
+ ignorePatternsByStore,
27636
27876
  configRootDir: rootDir,
27637
27877
  rootDir
27638
27878
  };
@@ -27640,8 +27880,8 @@ var resolveCheckoutConfig = async (startDir = process.cwd()) => {
27640
27880
  var collectLocalCheckoutFiles = async (rootDir, ignorePatterns, syncIncludePaths) => {
27641
27881
  return (await scanLocalCheckoutFiles(rootDir, ignorePatterns, syncIncludePaths)).files;
27642
27882
  };
27643
- var scanLocalCheckoutFiles = async (rootDir, ignorePatterns, syncIncludePaths, progress) => {
27644
- const matcher = createIgnoreMatcher(ignorePatterns);
27883
+ var scanLocalCheckoutFiles = async (rootDir, ignorePatterns, syncIncludePaths, progress, ignoreMatcher) => {
27884
+ const matcher = ignoreMatcher ?? createIgnoreMatcher(ignorePatterns);
27645
27885
  const includePaths = normalizeCheckoutIncludePaths(syncIncludePaths);
27646
27886
  const scan = {
27647
27887
  files: [],
@@ -27663,18 +27903,76 @@ var scanLocalCheckoutFiles = async (rootDir, ignorePatterns, syncIncludePaths, p
27663
27903
  );
27664
27904
  return scan;
27665
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
+ };
27666
27924
  var pullCheckout = async ({
27667
27925
  client,
27668
27926
  dryRun = false,
27669
27927
  filePaths,
27670
27928
  force = false,
27671
27929
  ignorePatterns,
27930
+ ignoreMatcher,
27931
+ ignoreMatchersByRule,
27932
+ ignoreSettings,
27933
+ ignorePatternsByStore,
27672
27934
  localPath,
27673
27935
  progress,
27674
27936
  storeRef,
27675
27937
  syncIncludePaths,
27676
27938
  syncRules
27677
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
+ }
27678
27976
  const requestedRelativePaths = normalizeCheckoutFilePaths(filePaths);
27679
27977
  if (syncRules) {
27680
27978
  await ensureManifestSyncRules(localPath, syncRules, {
@@ -27715,7 +28013,8 @@ var pullCheckout = async ({
27715
28013
  dryRun,
27716
28014
  filePaths: ruleFilePaths ?? void 0,
27717
28015
  force,
27718
- ignorePatterns,
28016
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
28017
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
27719
28018
  localPath,
27720
28019
  progress: checkoutProgressWithSyncRuleRemoteRefs(
27721
28020
  progress,
@@ -27746,7 +28045,7 @@ var pullCheckout = async ({
27746
28045
  );
27747
28046
  }
27748
28047
  const remoteEntries = [];
27749
- const matcher = createIgnoreMatcher(ignorePatterns);
28048
+ const matcher = ignoreMatcher ?? createIgnoreMatcher(ignorePatterns);
27750
28049
  const includePaths = normalizeCheckoutIncludePaths(syncIncludePaths);
27751
28050
  const manifest = await readCheckoutFilesManifest(localPath);
27752
28051
  const manifestSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -27857,7 +28156,7 @@ var pullCheckout = async ({
27857
28156
  }
27858
28157
  if (!dryRun) {
27859
28158
  const deletedLocalFile = await deleteLocalFileIfPresent(
27860
- path7.join(localPath, relativePath)
28159
+ path9.join(localPath, relativePath)
27861
28160
  );
27862
28161
  delete manifest.files[relativePath];
27863
28162
  manifest.references[relativePath] = {
@@ -27942,7 +28241,7 @@ var pullCheckout = async ({
27942
28241
  );
27943
28242
  }
27944
28243
  const bytes = new Uint8Array(await response.arrayBuffer());
27945
- await atomicWriteBinaryFile(path7.join(localPath, relativePath), bytes);
28244
+ await atomicWriteBinaryFile(path9.join(localPath, relativePath), bytes);
27946
28245
  updateFileManifestEntry({
27947
28246
  storageClass: "binary",
27948
28247
  hash: hashBytes(bytes),
@@ -27968,7 +28267,7 @@ var pullCheckout = async ({
27968
28267
  text: remoteFile.content.text
27969
28268
  });
27970
28269
  await atomicWriteTextFile(
27971
- path7.join(localPath, relativePath),
28270
+ path9.join(localPath, relativePath),
27972
28271
  localText,
27973
28272
  remoteFile.isExecutable === true
27974
28273
  );
@@ -28138,7 +28437,7 @@ var pullStoreFiles = async ({
28138
28437
  if (!remoteFile) {
28139
28438
  throw new Error("remote file disappeared while pulling");
28140
28439
  }
28141
- const destinationPath = path7.join(localPath, targetPath, relativePath);
28440
+ const destinationPath = path9.join(localPath, targetPath, relativePath);
28142
28441
  const changeType = await localPathExists(destinationPath) ? "updated" : "created";
28143
28442
  if (remoteFileHasBlobContent(remoteFile)) {
28144
28443
  const { downloadUrl } = await client.createFileDownloadUrlByRef({
@@ -28189,12 +28488,52 @@ var pushCheckout = async ({
28189
28488
  filePaths,
28190
28489
  force,
28191
28490
  ignorePatterns,
28491
+ ignoreMatcher,
28492
+ ignoreMatchersByRule,
28493
+ ignoreSettings,
28494
+ ignorePatternsByStore,
28192
28495
  localPath,
28193
28496
  progress,
28194
28497
  storeRef,
28195
28498
  syncIncludePaths,
28196
28499
  syncRules
28197
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
+ }
28198
28537
  const requestedRelativePaths = normalizeCheckoutFilePaths(filePaths);
28199
28538
  if (syncRules) {
28200
28539
  await ensureManifestSyncRules(localPath, syncRules, {
@@ -28235,7 +28574,8 @@ var pushCheckout = async ({
28235
28574
  dryRun,
28236
28575
  filePaths: ruleFilePaths ?? void 0,
28237
28576
  force,
28238
- ignorePatterns,
28577
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
28578
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
28239
28579
  localPath,
28240
28580
  progress: checkoutProgressWithSyncRuleRemoteRefs(
28241
28581
  progress,
@@ -28265,14 +28605,20 @@ var pushCheckout = async ({
28265
28605
  "upload"
28266
28606
  );
28267
28607
  }
28268
- const matcher = createIgnoreMatcher(ignorePatterns);
28608
+ const matcher = ignoreMatcher ?? createIgnoreMatcher(ignorePatterns);
28269
28609
  const includePaths = normalizeCheckoutIncludePaths(syncIncludePaths);
28270
28610
  const scan = requestedRelativePaths ? await scanTargetLocalCheckoutFiles(
28271
28611
  localPath,
28272
28612
  requestedRelativePaths,
28273
28613
  includePaths,
28274
28614
  matcher
28275
- ) : await scanLocalCheckoutFiles(localPath, ignorePatterns, includePaths);
28615
+ ) : await scanLocalCheckoutFiles(
28616
+ localPath,
28617
+ ignorePatterns,
28618
+ includePaths,
28619
+ void 0,
28620
+ matcher
28621
+ );
28276
28622
  const manifest = await readCheckoutFilesManifest(localPath);
28277
28623
  const manifestSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
28278
28624
  const localPaths = new Set(scan.files.map((file2) => file2.relativePath));
@@ -28532,7 +28878,7 @@ var pullRemoteFileIntoCheckout = async ({
28532
28878
  );
28533
28879
  }
28534
28880
  const bytes = new Uint8Array(await response.arrayBuffer());
28535
- await atomicWriteBinaryFile(path7.join(localPath, relativePath), bytes);
28881
+ await atomicWriteBinaryFile(path9.join(localPath, relativePath), bytes);
28536
28882
  updateFileManifestEntry({
28537
28883
  storageClass: "binary",
28538
28884
  hash: hashBytes(bytes),
@@ -28558,7 +28904,7 @@ var pullRemoteFileIntoCheckout = async ({
28558
28904
  text: remoteFile.content.text
28559
28905
  });
28560
28906
  await atomicWriteTextFile(
28561
- path7.join(localPath, relativePath),
28907
+ path9.join(localPath, relativePath),
28562
28908
  localText,
28563
28909
  remoteFile.isExecutable === true
28564
28910
  );
@@ -28651,7 +28997,7 @@ var remoteFileHash = async ({
28651
28997
  client,
28652
28998
  remoteFile,
28653
28999
  storeRef,
28654
- path: path20
29000
+ path: path22
28655
29001
  }) => {
28656
29002
  const textHash = remoteTextHash(remoteFile);
28657
29003
  if (textHash) {
@@ -28662,7 +29008,7 @@ var remoteFileHash = async ({
28662
29008
  }
28663
29009
  const { downloadUrl } = await client.createFileDownloadUrlByRef({
28664
29010
  ...storeRef,
28665
- path: path20
29011
+ path: path22
28666
29012
  });
28667
29013
  const response = await fetch(downloadUrl);
28668
29014
  if (!response.ok) {
@@ -28812,6 +29158,7 @@ var deleteRemoteFileFromCheckout = async ({
28812
29158
  };
28813
29159
  var executeSyncCheckoutFiles = async ({
28814
29160
  client,
29161
+ conflictPolicy,
28815
29162
  dryRun,
28816
29163
  force,
28817
29164
  localPath,
@@ -28836,6 +29183,7 @@ var executeSyncCheckoutFiles = async ({
28836
29183
  paths,
28837
29184
  (relativePath) => relativePath,
28838
29185
  async (relativePath) => syncCheckoutFile({
29186
+ conflictPolicy,
28839
29187
  client,
28840
29188
  dryRun,
28841
29189
  force,
@@ -28862,6 +29210,7 @@ var ignoredCheckoutFileResult = (ignored) => ({
28862
29210
  });
28863
29211
  var planSyncCheckoutSession = async ({
28864
29212
  client,
29213
+ conflictPolicy,
28865
29214
  dryRun,
28866
29215
  files,
28867
29216
  force,
@@ -28878,6 +29227,10 @@ var planSyncCheckoutSession = async ({
28878
29227
  remoteFileCount: files.length
28879
29228
  });
28880
29229
  const remoteRenameConflictDetails = await reconcileRemoteRenamesBeforePull({
29230
+ client,
29231
+ conflictPolicy,
29232
+ localFiles: scan.files,
29233
+ storeRef,
28881
29234
  dryRun,
28882
29235
  files,
28883
29236
  force,
@@ -28887,7 +29240,7 @@ var planSyncCheckoutSession = async ({
28887
29240
  const remoteRenameConflictPaths = checkoutDetailPathSet(
28888
29241
  remoteRenameConflictDetails
28889
29242
  );
28890
- const snapshot = buildSyncCheckoutSnapshot({
29243
+ let snapshot = buildSyncCheckoutSnapshot({
28891
29244
  includePaths,
28892
29245
  isIgnored: (relativePath) => isIgnored(relativePath, false, includePaths, matcher),
28893
29246
  localFiles: scan.files,
@@ -28903,6 +29256,9 @@ var planSyncCheckoutSession = async ({
28903
29256
  phase: "local-rename-preflight"
28904
29257
  });
28905
29258
  const structuralPreflight = await preflightLocalRenamesBeforePush({
29259
+ conflictPolicy,
29260
+ remoteFiles: files,
29261
+ localPath,
28906
29262
  client,
28907
29263
  deletePaths: snapshot.deletePaths,
28908
29264
  dryRun,
@@ -28910,6 +29266,26 @@ var planSyncCheckoutSession = async ({
28910
29266
  manifest,
28911
29267
  storeRef
28912
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
+ });
28913
29289
  const preflightDetails = [
28914
29290
  ...scan.ignored.map(ignoredCheckoutFileResult),
28915
29291
  ...scan.unsupported.map(unsupportedCheckoutFileResult)
@@ -28924,6 +29300,7 @@ var planSyncCheckoutSession = async ({
28924
29300
  });
28925
29301
  return {
28926
29302
  explicitDetails: [
29303
+ ...withCheckoutDetailsDirection(policyDetails, "download"),
28927
29304
  ...withCheckoutDetailsDirection(remoteRenameConflictDetails, "download"),
28928
29305
  ...withCheckoutDetailsDirection(
28929
29306
  structuralPreflight.movedDetails,
@@ -28959,6 +29336,7 @@ var readSyncCheckoutRemoteFiles = async ({
28959
29336
  };
28960
29337
  var scanSyncCheckoutLocalFiles = async ({
28961
29338
  ignorePatterns,
29339
+ ignoreMatcher,
28962
29340
  includePaths,
28963
29341
  localPath,
28964
29342
  progress
@@ -28967,7 +29345,8 @@ var scanSyncCheckoutLocalFiles = async ({
28967
29345
  localPath,
28968
29346
  ignorePatterns,
28969
29347
  includePaths,
28970
- progress
29348
+ progress,
29349
+ ignoreMatcher
28971
29350
  );
28972
29351
  reportCheckoutProgressEvent(
28973
29352
  progress,
@@ -29021,6 +29400,7 @@ var finalizeSyncCheckoutSession = async ({
29021
29400
  };
29022
29401
  var syncCheckoutFile = async ({
29023
29402
  client,
29403
+ conflictPolicy,
29024
29404
  dryRun,
29025
29405
  force,
29026
29406
  isInsideRemoteReference,
@@ -29035,7 +29415,7 @@ var syncCheckoutFile = async ({
29035
29415
  const manifestEntry = manifest.files[relativePath];
29036
29416
  const remoteKind = remoteFile ? isReferenceEntry(remoteFile.entry) ? "reference" : "file" : "missing";
29037
29417
  let remoteVersionChanged = false;
29038
- if (remoteKind === "file" && remoteFile && localFile && manifestEntry) {
29418
+ if (remoteKind === "file" && remoteFile && manifestEntry) {
29039
29419
  if (typeof remoteFile.entry.revisionNumber === "number") {
29040
29420
  remoteVersionChanged = (manifestEntry.remoteRevisionNumber ?? null) !== remoteFile.entry.revisionNumber;
29041
29421
  } else {
@@ -29049,18 +29429,19 @@ var syncCheckoutFile = async ({
29049
29429
  remoteVersionChanged = (manifestEntry.remoteRevisionNumber ?? null) !== remoteRevisionNumberForConflictCheck(remote);
29050
29430
  }
29051
29431
  }
29052
- if (isReferenceManifestEntry(manifestEntry)) {
29432
+ if (isReferenceManifestEntry(manifestEntry) && (!conflictPolicy || remoteKind === "missing")) {
29053
29433
  const localHash = localFile?.hash ?? null;
29054
29434
  const localChanged = localHash === null || localHash !== manifestEntry.hash;
29055
29435
  if (remoteKind === "missing") {
29056
- if (dryRun) {
29436
+ if (dryRun && !conflictPolicy) {
29057
29437
  return withCheckoutWorkerDirection(
29058
29438
  localChanged ? "deleted" : "ignored",
29059
29439
  "download"
29060
29440
  );
29061
29441
  }
29062
29442
  const result = await reconcileTrackedRemoteDelete({
29063
- force,
29443
+ dryRun,
29444
+ force: force || conflictPolicy === "remote",
29064
29445
  localFileHash: localHash,
29065
29446
  localPath,
29066
29447
  manifest,
@@ -29077,7 +29458,7 @@ var syncCheckoutFile = async ({
29077
29458
  return withCheckoutWorkerDirection(
29078
29459
  await deleteRemoteFileFromCheckout({
29079
29460
  client,
29080
- force,
29461
+ force: force || conflictPolicy === "local",
29081
29462
  manifest,
29082
29463
  manifestEntry,
29083
29464
  relativePath,
@@ -29107,7 +29488,7 @@ var syncCheckoutFile = async ({
29107
29488
  await pushLocalFileFromCheckout({
29108
29489
  client,
29109
29490
  file: localFile,
29110
- force,
29491
+ force: force || conflictPolicy === "local",
29111
29492
  manifest,
29112
29493
  manifestEntry,
29113
29494
  manifestSyncedAt,
@@ -29190,6 +29571,8 @@ var syncCheckoutFile = async ({
29190
29571
  }
29191
29572
  }
29192
29573
  const plan = planSyncCheckoutFile({
29574
+ resolvedConflict: remoteFile?.conflictWinner,
29575
+ conflictPolicy,
29193
29576
  force,
29194
29577
  localIsExecutable: localFile?.storageClass === "text" ? localFile.isExecutable : false,
29195
29578
  localHash: localFile?.hash ?? null,
@@ -29233,7 +29616,7 @@ var syncCheckoutFile = async ({
29233
29616
  if (!remoteFile || !isReferenceEntry(remoteFile.entry)) {
29234
29617
  throw new Error("planned reference accept without remote reference");
29235
29618
  }
29236
- 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;
29237
29620
  if (!dryRun) {
29238
29621
  delete manifest.files[relativePath];
29239
29622
  manifest.references[relativePath] = {
@@ -29252,7 +29635,7 @@ var syncCheckoutFile = async ({
29252
29635
  }
29253
29636
  const result = await reconcileTrackedRemoteDelete({
29254
29637
  dryRun,
29255
- force,
29638
+ force: force || conflictPolicy === "remote",
29256
29639
  localFileHash: localFile?.hash ?? null,
29257
29640
  localPath,
29258
29641
  manifest,
@@ -29262,7 +29645,11 @@ var syncCheckoutFile = async ({
29262
29645
  return typeof result === "string" || result.status !== "failed" ? withCheckoutWorkerDirection(result, "download") : result;
29263
29646
  }
29264
29647
  if (plan.type === "pullRemote") {
29265
- if (!remoteFile) {
29648
+ const pullEntry = remoteFile?.entry ?? (remotePathExists ? await client.readFileByRef({
29649
+ ...storeRef,
29650
+ path: toStorePath2(relativePath)
29651
+ }) : null);
29652
+ if (!pullEntry) {
29266
29653
  throw new Error("planned pull without remote file");
29267
29654
  }
29268
29655
  if (dryRun) {
@@ -29277,13 +29664,13 @@ var syncCheckoutFile = async ({
29277
29664
  return withCheckoutWorkerDirection(
29278
29665
  await pullRemoteFileIntoCheckout({
29279
29666
  client,
29280
- entry: remoteFile.entry,
29667
+ entry: pullEntry,
29281
29668
  localPath,
29282
29669
  manifest,
29283
29670
  manifestEntry,
29284
29671
  manifestSyncedAt,
29285
29672
  relativePath,
29286
- source: remoteFile.source,
29673
+ source: remoteFile?.source,
29287
29674
  storeRef
29288
29675
  }),
29289
29676
  "download"
@@ -29302,13 +29689,17 @@ var syncCheckoutFile = async ({
29302
29689
  "upload"
29303
29690
  );
29304
29691
  }
29692
+ const pushManifestEntry = conflictPolicy === "local" && manifestEntry?.storageClass === "binary" && remoteFile ? {
29693
+ ...manifestEntry,
29694
+ remoteRevisionNumber: remoteFile.entry.revisionNumber
29695
+ } : manifestEntry;
29305
29696
  return withCheckoutWorkerDirection(
29306
29697
  await pushLocalFileFromCheckout({
29307
29698
  client,
29308
29699
  file: localFile,
29309
- force,
29700
+ force: force || conflictPolicy === "local",
29310
29701
  manifest,
29311
- manifestEntry,
29702
+ manifestEntry: pushManifestEntry,
29312
29703
  manifestSyncedAt,
29313
29704
  storeRef
29314
29705
  }),
@@ -29325,7 +29716,7 @@ var syncCheckoutFile = async ({
29325
29716
  return withCheckoutWorkerDirection(
29326
29717
  await deleteRemoteFileFromCheckout({
29327
29718
  client,
29328
- force,
29719
+ force: force || conflictPolicy === "local",
29329
29720
  manifest,
29330
29721
  manifestEntry,
29331
29722
  relativePath,
@@ -29338,16 +29729,18 @@ var syncCheckoutFile = async ({
29338
29729
  };
29339
29730
  var syncCheckoutRuleFiles = async ({
29340
29731
  client,
29732
+ conflictPolicy,
29341
29733
  dryRun,
29342
29734
  force,
29343
29735
  ignorePatterns,
29736
+ ignoreMatcher,
29344
29737
  localPath,
29345
29738
  progress,
29346
29739
  referenceSyncMode,
29347
29740
  storeRef,
29348
29741
  syncIncludePaths
29349
29742
  }) => {
29350
- const matcher = createIgnoreMatcher(ignorePatterns);
29743
+ const matcher = ignoreMatcher ?? createIgnoreMatcher(ignorePatterns);
29351
29744
  const includePaths = normalizeCheckoutIncludePaths(syncIncludePaths);
29352
29745
  const manifest = await readCheckoutFilesManifest(localPath);
29353
29746
  const manifestSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -29358,12 +29751,14 @@ var syncCheckoutRuleFiles = async ({
29358
29751
  storeRef
29359
29752
  });
29360
29753
  const scan = await scanSyncCheckoutLocalFiles({
29754
+ ignoreMatcher,
29361
29755
  ignorePatterns,
29362
29756
  includePaths,
29363
29757
  localPath,
29364
29758
  progress
29365
29759
  });
29366
29760
  const sessionPlan = await planSyncCheckoutSession({
29761
+ conflictPolicy,
29367
29762
  client,
29368
29763
  dryRun,
29369
29764
  files,
@@ -29385,6 +29780,7 @@ var syncCheckoutRuleFiles = async ({
29385
29780
  progress?.onTotal?.(explicitDetails.length + paths.length);
29386
29781
  reportCheckoutDetails(progress, explicitDetails);
29387
29782
  const result = await executeSyncCheckoutFiles({
29783
+ conflictPolicy,
29388
29784
  client,
29389
29785
  dryRun,
29390
29786
  force,
@@ -29416,9 +29812,14 @@ var syncCheckoutRuleFiles = async ({
29416
29812
  var syncCheckout = async ({
29417
29813
  beforeRuleApply,
29418
29814
  client,
29815
+ conflictPolicy,
29419
29816
  dryRun = false,
29420
29817
  force,
29421
29818
  ignorePatterns,
29819
+ ignoreMatcher,
29820
+ ignoreMatchersByRule,
29821
+ ignoreSettings,
29822
+ ignorePatternsByStore,
29422
29823
  localPath,
29423
29824
  progress,
29424
29825
  referenceSyncMode = false,
@@ -29426,6 +29827,44 @@ var syncCheckout = async ({
29426
29827
  syncIncludePaths,
29427
29828
  syncRules
29428
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
+ }
29429
29868
  if (syncRules) {
29430
29869
  reportCheckoutProgressEvent(progress, "Checking checkout sync rules", {
29431
29870
  phase: "sync-rules",
@@ -29525,15 +29964,18 @@ var syncCheckout = async ({
29525
29964
  client: ruleClient,
29526
29965
  dryRun: true,
29527
29966
  force,
29528
- ignorePatterns,
29967
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
29968
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
29529
29969
  localPath,
29530
29970
  storeRef: ruleStoreRef,
29531
29971
  syncIncludePaths: ruleIncludePaths
29532
29972
  }) : await syncCheckoutRuleFiles({
29973
+ conflictPolicy,
29533
29974
  client: ruleClient,
29534
29975
  dryRun: true,
29535
29976
  force,
29536
- ignorePatterns,
29977
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
29978
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
29537
29979
  localPath,
29538
29980
  referenceSyncMode: isStoreReferenceRule,
29539
29981
  storeRef: ruleStoreRef,
@@ -29557,16 +29999,19 @@ var syncCheckout = async ({
29557
29999
  client: ruleClient,
29558
30000
  dryRun,
29559
30001
  force,
29560
- ignorePatterns,
30002
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
30003
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
29561
30004
  localPath,
29562
30005
  progress: ruleProgress,
29563
30006
  storeRef: ruleStoreRef,
29564
30007
  syncIncludePaths: ruleIncludePaths
29565
30008
  }) : await syncCheckoutRuleFiles({
30009
+ conflictPolicy,
29566
30010
  client: ruleClient,
29567
30011
  dryRun,
29568
30012
  force,
29569
- ignorePatterns,
30013
+ ignorePatterns: ignorePatternsByStore?.[rule.store] ?? ignorePatterns,
30014
+ ignoreMatcher: ignoreMatchersByRule?.[rule.id],
29570
30015
  localPath,
29571
30016
  progress: ruleProgress,
29572
30017
  referenceSyncMode: isStoreReferenceRule,
@@ -29621,6 +30066,8 @@ var syncCheckout = async ({
29621
30066
  );
29622
30067
  }
29623
30068
  return syncCheckoutRuleFiles({
30069
+ ignoreMatcher,
30070
+ conflictPolicy,
29624
30071
  client,
29625
30072
  dryRun,
29626
30073
  force,
@@ -29633,16 +30080,16 @@ var syncCheckout = async ({
29633
30080
  });
29634
30081
  };
29635
30082
  var ensureCheckoutDirectoryReady = async (directoryPath) => {
29636
- await mkdir3(directoryPath, { recursive: true });
30083
+ await mkdir4(directoryPath, { recursive: true });
29637
30084
  const entries = await readdir2(directoryPath);
29638
30085
  if (entries.length > 0) {
29639
30086
  throw new Error(`Target directory must be empty: ${directoryPath}`);
29640
30087
  }
29641
30088
  };
29642
30089
  var ensureProjectConfigDoesNotExist = async (directoryPath) => {
29643
- for (const filename of reservedRootFileNames) {
30090
+ for (const filename of projectConfigFilenames) {
29644
30091
  try {
29645
- const filePath = path7.join(directoryPath, filename);
30092
+ const filePath = path9.join(directoryPath, filename);
29646
30093
  await stat(filePath);
29647
30094
  throw new Error(`Forum config already exists: ${filePath}`);
29648
30095
  } catch (error51) {
@@ -29655,142 +30102,40 @@ var ensureProjectConfigDoesNotExist = async (directoryPath) => {
29655
30102
  };
29656
30103
 
29657
30104
  // src/ignoreCommand.ts
29658
- import ignore2 from "ignore";
29659
- var toRelative = (storePath) => storePath.replace(/^\/+/, "");
29660
- var sortPaths = (paths) => [...paths].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
29661
- var planIgnoreAdd = (pattern, currentPatterns, remoteFilePaths) => {
29662
- const trimmed = pattern.trim();
29663
- if (!trimmed) {
29664
- throw new Error("Ignore pattern cannot be empty.");
29665
- }
29666
- const alreadyPresent = currentPatterns.includes(trimmed);
29667
- const nextPatterns = resolveIgnorePatterns({
29668
- bakedIn: [],
29669
- storeSetting: currentPatterns,
29670
- localOverride: [trimmed]
29671
- });
29672
- const matcher = ignore2().add([trimmed]);
29673
- const matchedRemotePaths = sortPaths(
29674
- remoteFilePaths.filter((storePath) => {
29675
- const relative = toRelative(storePath);
29676
- return Boolean(relative) && matcher.ignores(relative);
29677
- })
29678
- );
29679
- return { pattern: trimmed, alreadyPresent, nextPatterns, matchedRemotePaths };
29680
- };
29681
- var collectRemoteFilePaths = async (client, storeRef) => {
29682
- const listing = await client.listTreeByRef({
29683
- ...storeRef,
29684
- directoryPath: "/",
29685
- includeHidden: true
29686
- });
29687
- const entries = listing?.entries ?? [];
29688
- return sortPaths(
29689
- entries.filter((entry) => entry.type !== "directory").map((entry) => entry.path)
29690
- );
29691
- };
29692
- var executeIgnoreAdd = async (client, storeRef, pattern, currentPatterns, io) => {
29693
- const remoteFilePaths = await collectRemoteFilePaths(client, storeRef);
29694
- const plan = planIgnoreAdd(pattern, currentPatterns, remoteFilePaths);
29695
- let settingUpdated = false;
29696
- if (!plan.alreadyPresent) {
29697
- await client.upsertStoreByRef({
29698
- ...storeRef,
29699
- patch: { ignore: plan.nextPatterns }
29700
- });
29701
- settingUpdated = true;
29702
- }
29703
- const deletedRemotePaths = [];
29704
- let deletedRemoteDirectories = [];
29705
- if (plan.matchedRemotePaths.length > 0 && await io.confirmRemoteDelete(plan)) {
29706
- for (const storePath of plan.matchedRemotePaths) {
29707
- await client.deleteFileByRef({ ...storeRef, path: storePath });
29708
- deletedRemotePaths.push(storePath);
29709
- }
29710
- deletedRemoteDirectories = await cleanupRemoteCheckoutDirectories({
29711
- client,
29712
- removedPaths: deletedRemotePaths.map(toRelative),
29713
- storeRef
29714
- });
29715
- }
30105
+ var planIgnoreAdd = (pattern, currentPatterns) => {
30106
+ if (!pattern.trim()) throw new Error("Ignore pattern cannot be empty.");
30107
+ const alreadyPresent = currentPatterns.at(-1) === pattern;
29716
30108
  return {
29717
- plan,
29718
- settingUpdated,
29719
- deletedRemotePaths,
29720
- deletedRemoteDirectories
30109
+ pattern,
30110
+ alreadyPresent,
30111
+ nextPatterns: alreadyPresent ? currentPatterns : [...currentPatterns, pattern]
29721
30112
  };
29722
30113
  };
29723
30114
  var removeIgnorePattern = (pattern, currentPatterns) => {
29724
- const trimmed = pattern.trim();
29725
- if (!trimmed) {
29726
- throw new Error("Ignore pattern cannot be empty.");
29727
- }
29728
- 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);
29729
30117
  return {
29730
30118
  nextPatterns,
29731
30119
  removed: nextPatterns.length !== currentPatterns.length,
29732
- pattern: trimmed
30120
+ pattern
29733
30121
  };
29734
30122
  };
29735
- var setIgnorePatterns = (patterns) => resolveIgnorePatterns({
29736
- bakedIn: [],
29737
- storeSetting: [],
29738
- localOverride: patterns
29739
- });
30123
+ var setIgnorePatterns = (patterns) => {
30124
+ if (patterns.some((pattern) => !pattern.trim()))
30125
+ throw new Error("Ignore pattern cannot be empty.");
30126
+ return patterns;
30127
+ };
29740
30128
 
29741
30129
  // src/runIgnoreCommand.ts
29742
30130
  var runAdd = async (deps) => {
29743
- const {
29744
- client,
29745
- storeRef,
29746
- currentPatterns,
29747
- pattern,
29748
- assumeYes,
29749
- confirmYesNo: confirmYesNo2,
29750
- renderOutput: renderOutput2,
29751
- commandNames: commandNames2
29752
- } = deps;
29753
- if (!pattern) {
29754
- throw new Error(`Usage: ${commandNames2} ignore add <pattern>`);
29755
- }
29756
- const io = {
29757
- confirmRemoteDelete: (plan) => confirmYesNo2({
29758
- assumeYes,
29759
- nonInteractiveMessage: "Refusing to delete store files without confirmation. Re-run with --yes.",
29760
- prompt: `Delete ${plan.matchedRemotePaths.length} matching file(s) from the store? [y/N] `
29761
- })
29762
- };
29763
- const result = await executeIgnoreAdd(
29764
- client,
29765
- storeRef,
29766
- pattern,
29767
- currentPatterns,
29768
- io
29769
- );
29770
- if (result.settingUpdated) {
29771
- renderOutput2(`Added ignore rule: ${result.plan.pattern}`);
29772
- } else {
29773
- renderOutput2(`Ignore rule already present: ${result.plan.pattern}`);
29774
- }
29775
- if (result.plan.matchedRemotePaths.length === 0) {
29776
- renderOutput2("No already-pushed files matched.");
29777
- } else if (result.deletedRemotePaths.length > 0) {
29778
- renderOutput2(
29779
- `Deleted ${result.deletedRemotePaths.length} file(s) from the store.`
29780
- );
29781
- } else {
29782
- renderOutput2("Left already-pushed matches in place.");
29783
- }
29784
- if (result.deletedRemoteDirectories.length > 0) {
29785
- renderOutput2(
29786
- `Deleted ${result.deletedRemoteDirectories.length} empty director${result.deletedRemoteDirectories.length === 1 ? "y" : "ies"} from the store.`
29787
- );
29788
- }
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.`);
29789
30136
  };
29790
30137
  var runList = async (deps) => {
29791
- const { client, storeRef, renderOutput: renderOutput2 } = deps;
29792
- const metadata = await client.readStoreByRef(storeRef);
29793
- const ignore3 = metadata?.ignore ?? [];
30138
+ const { currentPatterns: ignore3, renderOutput: renderOutput2 } = deps;
29794
30139
  if (ignore3.length === 0) {
29795
30140
  renderOutput2("(no ignore rules)");
29796
30141
  return;
@@ -29801,8 +30146,7 @@ var runList = async (deps) => {
29801
30146
  };
29802
30147
  var runRemove = async (deps) => {
29803
30148
  const {
29804
- client,
29805
- storeRef,
30149
+ writePatterns,
29806
30150
  currentPatterns,
29807
30151
  pattern,
29808
30152
  renderOutput: renderOutput2,
@@ -29816,14 +30160,11 @@ var runRemove = async (deps) => {
29816
30160
  renderOutput2(`Ignore rule not present: ${result.pattern}`);
29817
30161
  return;
29818
30162
  }
29819
- await client.upsertStoreByRef({
29820
- ...storeRef,
29821
- patch: { ignore: result.nextPatterns }
29822
- });
30163
+ await writePatterns(result.nextPatterns);
29823
30164
  renderOutput2(`Removed ignore rule: ${result.pattern}`);
29824
30165
  };
29825
30166
  var runSet = async (deps) => {
29826
- const { client, storeRef, patterns, renderOutput: renderOutput2, commandNames: commandNames2 } = deps;
30167
+ const { writePatterns, patterns, renderOutput: renderOutput2, commandNames: commandNames2 } = deps;
29827
30168
  if (patterns.length === 0) {
29828
30169
  throw new Error(`Usage: ${commandNames2} ignore set <pattern>...`);
29829
30170
  }
@@ -29831,24 +30172,17 @@ var runSet = async (deps) => {
29831
30172
  if (nextPatterns.length === 0) {
29832
30173
  throw new Error("Ignore pattern cannot be empty.");
29833
30174
  }
29834
- await client.upsertStoreByRef({
29835
- ...storeRef,
29836
- patch: { ignore: nextPatterns }
29837
- });
30175
+ await writePatterns(nextPatterns);
29838
30176
  renderOutput2(`Set ${nextPatterns.length} ignore rule(s).`);
29839
30177
  };
29840
30178
  var runClear = async (deps) => {
29841
- const { client, storeRef, renderOutput: renderOutput2 } = deps;
29842
- await client.upsertStoreByRef({
29843
- ...storeRef,
29844
- patch: { ignore: [] }
29845
- });
30179
+ const { writePatterns, renderOutput: renderOutput2 } = deps;
30180
+ await writePatterns([]);
29846
30181
  renderOutput2("Cleared ignore rules.");
29847
30182
  };
29848
30183
  var runAuto = async (deps) => {
29849
30184
  const {
29850
- client,
29851
- storeRef,
30185
+ writePatterns,
29852
30186
  currentPatterns,
29853
30187
  assumeYes,
29854
30188
  confirmYesNo: confirmYesNo2,
@@ -29857,9 +30191,9 @@ var runAuto = async (deps) => {
29857
30191
  renderOutput: renderOutput2
29858
30192
  } = deps;
29859
30193
  const analysis = await analyzeLocal();
29860
- const additions = analysis.proposedPatterns.filter(
29861
- (proposed) => !currentPatterns.includes(proposed)
29862
- );
30194
+ const proposed = analysis.proposedPatterns;
30195
+ const alreadyAppended = JSON.stringify(currentPatterns.slice(-proposed.length)) === JSON.stringify(proposed);
30196
+ const additions = alreadyAppended ? [] : proposed;
29863
30197
  if (additions.length === 0) {
29864
30198
  renderOutput2("Nothing to suggest \u2014 no new junk patterns detected.");
29865
30199
  await markOnboarded();
@@ -29872,19 +30206,18 @@ var runAuto = async (deps) => {
29872
30206
  const confirmed = await confirmYesNo2({
29873
30207
  assumeYes,
29874
30208
  nonInteractiveMessage: "Refusing to write ignore rules without confirmation. Re-run with --yes.",
29875
- 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] `
29876
30210
  });
29877
30211
  if (!confirmed) {
29878
- renderOutput2("Left the store setting unchanged.");
30212
+ renderOutput2("Left the shared ignore file unchanged.");
29879
30213
  await markOnboarded();
29880
30214
  return;
29881
30215
  }
29882
- await client.upsertStoreByRef({
29883
- ...storeRef,
29884
- patch: { ignore: [...currentPatterns, ...additions] }
29885
- });
30216
+ await writePatterns([...currentPatterns, ...additions]);
29886
30217
  await markOnboarded();
29887
- 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
+ );
29888
30221
  };
29889
30222
  var runIgnoreCommand = async (deps) => {
29890
30223
  const { subcommand, commandNames: commandNames2 } = deps;
@@ -29941,8 +30274,7 @@ var warnImminent = (analysis, renderWarning2) => {
29941
30274
  };
29942
30275
  var maybePromptIgnoreSetup = async (deps) => {
29943
30276
  const {
29944
- client,
29945
- storeRef,
30277
+ writePatterns,
29946
30278
  currentPatterns,
29947
30279
  analysis,
29948
30280
  onboarded,
@@ -29983,13 +30315,10 @@ var maybePromptIgnoreSetup = async (deps) => {
29983
30315
  }
29984
30316
  const action = await chooseAction(analysis);
29985
30317
  if (action === "auto") {
29986
- await client.upsertStoreByRef({
29987
- ...storeRef,
29988
- patch: { ignore: analysis.proposedPatterns }
29989
- });
30318
+ await writePatterns(analysis.proposedPatterns);
29990
30319
  await markOnboarded();
29991
30320
  renderOutput2(
29992
- `Added ${analysis.proposedPatterns.length} ignore rule(s) to the store.`
30321
+ `Added ${analysis.proposedPatterns.length} shared ignore rule(s) locally.`
29993
30322
  );
29994
30323
  return {
29995
30324
  outcome: "auto-applied",
@@ -30016,10 +30345,10 @@ var maybePromptIgnoreSetup = async (deps) => {
30016
30345
 
30017
30346
  // src/collectIgnoreMarkers.ts
30018
30347
  import { stat as stat2 } from "fs/promises";
30019
- import path8 from "path";
30348
+ import path10 from "path";
30020
30349
  var exists = async (rootDir, rule) => {
30021
30350
  try {
30022
- const stats = await stat2(path8.join(rootDir, rule.marker));
30351
+ const stats = await stat2(path10.join(rootDir, rule.marker));
30023
30352
  return rule.markerKind === "dir" ? stats.isDirectory() : stats.isFile();
30024
30353
  } catch {
30025
30354
  return false;
@@ -30038,11 +30367,11 @@ var collectIgnoreMarkers = async (rootDir, rules = IGNORE_PROPOSAL_RULES) => {
30038
30367
  };
30039
30368
 
30040
30369
  // src/readGitignorePatterns.ts
30041
- import { readFile as readFile3 } from "fs/promises";
30042
- import path9 from "path";
30370
+ import { readFile as readFile4 } from "fs/promises";
30371
+ import path11 from "path";
30043
30372
  var readGitignorePatterns = async (rootDir) => {
30044
30373
  try {
30045
- const raw = await readFile3(path9.join(rootDir, ".gitignore"), "utf8");
30374
+ const raw = await readFile4(path11.join(rootDir, ".gitignore"), "utf8");
30046
30375
  return parseLocalIgnoreFile(raw);
30047
30376
  } catch {
30048
30377
  return [];
@@ -30106,9 +30435,9 @@ var createHumanWatchSubscriber = ({
30106
30435
  return;
30107
30436
  }
30108
30437
  case "sync.watch.conflict.raised": {
30109
- const path20 = String(details.path ?? "");
30438
+ const path22 = String(details.path ?? "");
30110
30439
  const suggestions = details.suggestions ?? [];
30111
- error51(`Conflict: ${path20} \u2014 ${event.message ?? "needs attention"}`);
30440
+ error51(`Conflict: ${path22} \u2014 ${event.message ?? "needs attention"}`);
30112
30441
  if (suggestions.length > 0) {
30113
30442
  error51(` ${formatSuggestions(suggestions)}`);
30114
30443
  }
@@ -30137,10 +30466,10 @@ import notifier from "node-notifier";
30137
30466
  var createNotifyWatchSubscriber = () => {
30138
30467
  return (event) => {
30139
30468
  if (event.eventType === "sync.watch.conflict.raised") {
30140
- const path20 = event.details?.path;
30469
+ const path22 = event.details?.path;
30141
30470
  notifier.notify({
30142
30471
  title: "Forum sync conflict",
30143
- 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"
30144
30473
  });
30145
30474
  return;
30146
30475
  }
@@ -30256,10 +30585,10 @@ var createConflictTracker = () => {
30256
30585
  );
30257
30586
  const raised = current.filter((conflict) => !active.has(conflict.path));
30258
30587
  const cleared = [...active.keys()].filter(
30259
- (path20) => !currentByPath.has(path20)
30588
+ (path22) => !currentByPath.has(path22)
30260
30589
  );
30261
- for (const path20 of cleared) {
30262
- active.delete(path20);
30590
+ for (const path22 of cleared) {
30591
+ active.delete(path22);
30263
30592
  }
30264
30593
  for (const conflict of current) {
30265
30594
  active.set(conflict.path, conflict);
@@ -30270,13 +30599,13 @@ var createConflictTracker = () => {
30270
30599
  };
30271
30600
 
30272
30601
  // src/watch/conflictResolutionHints.ts
30273
- var keepLocal = (path20) => ({
30602
+ var keepLocal = (path22) => ({
30274
30603
  label: "Keep local",
30275
- command: `fm push --force ${path20}`
30604
+ command: `fm push --force ${path22}`
30276
30605
  });
30277
- var acceptRemote = (path20) => ({
30606
+ var acceptRemote = (path22) => ({
30278
30607
  label: "Accept remote",
30279
- command: `fm pull --force ${path20}`
30608
+ command: `fm pull --force ${path22}`
30280
30609
  });
30281
30610
  var review = (label, command) => ({
30282
30611
  label,
@@ -30292,23 +30621,23 @@ var forceResolvableTypes = /* @__PURE__ */ new Set([
30292
30621
  "reference-local-file",
30293
30622
  "read-only-local-change"
30294
30623
  ]);
30295
- var conflictResolutionSuggestions = (conflictType, path20) => {
30624
+ var conflictResolutionSuggestions = (conflictType, path22) => {
30296
30625
  if (forceResolvableTypes.has(conflictType)) {
30297
- return [keepLocal(path20), acceptRemote(path20)];
30626
+ return [keepLocal(path22), acceptRemote(path22)];
30298
30627
  }
30299
- return [review("Review", `fm status ${path20}`)];
30628
+ return [review("Review", `fm status ${path22}`)];
30300
30629
  };
30301
30630
 
30302
30631
  // src/watch/extractWatchConflicts.ts
30303
30632
  var toWatchConflict = (issue2) => {
30304
- const { path: path20, message, relatedPaths } = issue2.details.details;
30633
+ const { path: path22, message, relatedPaths } = issue2.details.details;
30305
30634
  const conflictType = issue2.details.type;
30306
30635
  return {
30307
- path: path20,
30636
+ path: path22,
30308
30637
  conflictType,
30309
30638
  message,
30310
30639
  relatedPaths: relatedPaths ?? [],
30311
- suggestions: conflictResolutionSuggestions(conflictType, path20)
30640
+ suggestions: conflictResolutionSuggestions(conflictType, path22)
30312
30641
  };
30313
30642
  };
30314
30643
  var extractWatchConflicts = (result) => {
@@ -30399,9 +30728,9 @@ var createCheckoutWatcher = ({
30399
30728
  })
30400
30729
  );
30401
30730
  }
30402
- for (const path20 of cleared) {
30731
+ for (const path22 of cleared) {
30403
30732
  bus.emit(
30404
- watchEvent("sync.watch.conflict.cleared", void 0, { path: path20 })
30733
+ watchEvent("sync.watch.conflict.cleared", void 0, { path: path22 })
30405
30734
  );
30406
30735
  }
30407
30736
  bus.emit(
@@ -30619,7 +30948,7 @@ var runSyncWatchDirectory = async (options2) => {
30619
30948
  };
30620
30949
 
30621
30950
  // src/watch/fleetStatusSubscriber.ts
30622
- import path10 from "path";
30951
+ import path12 from "path";
30623
30952
  var STATUS_BADGE = {
30624
30953
  starting: "\u2026",
30625
30954
  clean: "\u2713",
@@ -30637,15 +30966,15 @@ var STATUS_COLOR = {
30637
30966
  var deriveLabels = (rootDirs) => {
30638
30967
  const counts = /* @__PURE__ */ new Map();
30639
30968
  for (const dir of rootDirs) {
30640
- const base = path10.basename(dir);
30969
+ const base = path12.basename(dir);
30641
30970
  counts.set(base, (counts.get(base) ?? 0) + 1);
30642
30971
  }
30643
30972
  const labels = /* @__PURE__ */ new Map();
30644
30973
  for (const dir of rootDirs) {
30645
- const base = path10.basename(dir);
30974
+ const base = path12.basename(dir);
30646
30975
  labels.set(
30647
30976
  dir,
30648
- (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
30649
30978
  );
30650
30979
  }
30651
30980
  return labels;
@@ -30744,7 +31073,7 @@ var createFleetStatusSubscriber = (rootDirs, { write, isTty }) => {
30744
31073
  };
30745
31074
 
30746
31075
  // src/watch/discoverCheckouts.ts
30747
- import path11 from "path";
31076
+ import path13 from "path";
30748
31077
  import { readdir as readdir3, stat as stat3 } from "fs/promises";
30749
31078
  var pathExists2 = async (candidate) => {
30750
31079
  try {
@@ -30757,7 +31086,7 @@ var pathExists2 = async (candidate) => {
30757
31086
  var isCheckoutDir = async (dir) => {
30758
31087
  let hasStateDirectory = false;
30759
31088
  for (const name of checkoutStateDirectoryNames) {
30760
- if (await pathExists2(path11.join(dir, name))) {
31089
+ if (await pathExists2(path13.join(dir, name))) {
30761
31090
  hasStateDirectory = true;
30762
31091
  break;
30763
31092
  }
@@ -30766,7 +31095,7 @@ var isCheckoutDir = async (dir) => {
30766
31095
  return false;
30767
31096
  }
30768
31097
  for (const filename of projectConfigFilenames) {
30769
- if (await pathExists2(path11.join(dir, filename))) {
31098
+ if (await pathExists2(path13.join(dir, filename))) {
30770
31099
  return true;
30771
31100
  }
30772
31101
  }
@@ -30781,10 +31110,10 @@ var readSubdirectories = async (dir) => {
30781
31110
  }
30782
31111
  return entries.filter(
30783
31112
  (entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules"
30784
- ).map((entry) => path11.join(dir, entry.name));
31113
+ ).map((entry) => path13.join(dir, entry.name));
30785
31114
  };
30786
31115
  var discoverCheckouts = async (rootPath) => {
30787
- const root = path11.resolve(rootPath);
31116
+ const root = path13.resolve(rootPath);
30788
31117
  const found = [];
30789
31118
  const visit = async (dir) => {
30790
31119
  if (await isCheckoutDir(dir)) {
@@ -30799,51 +31128,27 @@ var discoverCheckouts = async (rootPath) => {
30799
31128
  };
30800
31129
 
30801
31130
  // src/watch/isIgnoredWatchPath.ts
30802
- import path12 from "path";
30803
- var ignoredWatchSegments = /* @__PURE__ */ new Set([
30804
- ".forum",
30805
- ".doctrine",
30806
- ".git",
30807
- "node_modules",
30808
- // Standard build/cache junk — never source content, and the usual offenders
30809
- // for high-churn writes during a build or test run.
30810
- ".cache",
30811
- ".next",
30812
- ".nuxt",
30813
- ".turbo",
30814
- ".svelte-kit",
30815
- "dist",
30816
- "build",
30817
- "coverage",
30818
- "out"
30819
- ]);
30820
- var ignoredRootFiles = /* @__PURE__ */ new Set([
30821
- "forum.yaml",
30822
- "forum.yml",
30823
- "doctrine.yaml",
30824
- "doctrine.yml"
30825
- ]);
31131
+ import path14 from "path";
30826
31132
  var isIgnoredWatchPath = (filename) => {
30827
- if (!filename) {
30828
- return false;
30829
- }
30830
- const relativePath = String(filename).split(path12.sep).join("/");
30831
- const segments2 = relativePath.split("/").filter((segment) => segment !== "");
30832
- if (segments2.some((segment) => ignoredWatchSegments.has(segment))) {
30833
- return true;
30834
- }
30835
- 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));
30836
31141
  };
30837
31142
 
30838
31143
  // src/watch/readPathsFile.ts
30839
- import { readFile as readFile4 } from "fs/promises";
31144
+ import { readFile as readFile5 } from "fs/promises";
30840
31145
  var readPathsFile = async (file2) => {
30841
- const contents = await readFile4(file2, "utf8");
31146
+ const contents = await readFile5(file2, "utf8");
30842
31147
  return contents.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
30843
31148
  };
30844
31149
 
30845
31150
  // src/watch/resolveWatchWorkingSet.ts
30846
- import path13 from "path";
31151
+ import path15 from "path";
30847
31152
  var resolveWatchWorkingSet = async ({ watchPaths, pathsFile }, resolvers) => {
30848
31153
  const explicitPaths = [...watchPaths];
30849
31154
  if (pathsFile !== null) {
@@ -30866,7 +31171,7 @@ var resolveWatchWorkingSet = async ({ watchPaths, pathsFile }, resolvers) => {
30866
31171
  const entries = [];
30867
31172
  const errors = [];
30868
31173
  for (const rawPath of explicitPaths) {
30869
- const resolved = path13.resolve(rawPath);
31174
+ const resolved = path15.resolve(rawPath);
30870
31175
  if (seen.has(resolved)) {
30871
31176
  continue;
30872
31177
  }
@@ -31102,6 +31407,10 @@ var createStoreClient = ({
31102
31407
  createStore: (input) => call("stores.create", () => trpc.stores.create.mutate(input)),
31103
31408
  deleteStore: (input) => call("stores.delete", () => trpc.stores.delete.mutate(input)),
31104
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
+ ),
31105
31414
  upsertStoreByRef: (input) => call("stores.upsertByRef", () => trpc.stores.upsertByRef.mutate(input)),
31106
31415
  deleteFileByRef: (input) => call(
31107
31416
  "files.deleteFileByRef",
@@ -31233,10 +31542,9 @@ var createClient = (config2) => {
31233
31542
 
31234
31543
  // src/state.ts
31235
31544
  import { homedir } from "os";
31236
- import path14 from "path";
31237
- 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";
31238
31547
  var DEFAULT_STATE_DIRNAME = ".forum";
31239
- var LEGACY_STATE_DIRNAME = ".doctrine";
31240
31548
  var DEFAULT_POLL_INTERVAL_MS = 4e3;
31241
31549
  var DEFAULT_API_BASE_URL = "https://api.forumlabs.net";
31242
31550
  var DEFAULT_CONFIG = {
@@ -31244,49 +31552,32 @@ var DEFAULT_CONFIG = {
31244
31552
  pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
31245
31553
  forceWrites: false
31246
31554
  };
31247
- var legacyStateDir = () => path14.join(homedir(), LEGACY_STATE_DIRNAME);
31248
- var defaultStateDir = () => path14.join(homedir(), DEFAULT_STATE_DIRNAME);
31249
- 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");
31250
31557
  var ensureStateDir = async (stateDir) => {
31251
- await mkdir4(stateDir, { recursive: true });
31558
+ await mkdir5(stateDir, { recursive: true });
31252
31559
  };
31253
31560
  var readJson = async (filePath, fallback) => {
31254
31561
  try {
31255
- const raw = await readFile5(filePath, "utf8");
31562
+ const raw = await readFile6(filePath, "utf8");
31256
31563
  return JSON.parse(raw);
31257
31564
  } catch {
31258
31565
  return fallback;
31259
31566
  }
31260
31567
  };
31261
- var fileExists2 = async (filePath) => {
31262
- try {
31263
- await access2(filePath);
31264
- return true;
31265
- } catch {
31266
- return false;
31267
- }
31268
- };
31269
- var readConfigSourcePath = async (stateDir) => {
31270
- const currentPath = configPath(stateDir);
31271
- if (stateDir !== defaultStateDir() || await fileExists2(currentPath)) {
31272
- return currentPath;
31273
- }
31274
- const legacyPath = configPath(legacyStateDir());
31275
- return await fileExists2(legacyPath) ? legacyPath : currentPath;
31276
- };
31277
31568
  var writeJsonAtomic = async (filePath, value) => {
31278
- const parentDir = path14.dirname(filePath);
31279
- const tempPath = path14.join(
31569
+ const parentDir = path16.dirname(filePath);
31570
+ const tempPath = path16.join(
31280
31571
  parentDir,
31281
- `.${path14.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
31572
+ `.${path16.basename(filePath)}.${process.pid}.${Date.now()}.tmp`
31282
31573
  );
31283
- await mkdir4(parentDir, { recursive: true });
31574
+ await mkdir5(parentDir, { recursive: true });
31284
31575
  await writeFile4(tempPath, JSON.stringify(value, null, 2), "utf8");
31285
31576
  await rename3(tempPath, filePath);
31286
31577
  };
31287
31578
  var readConfig = async (stateDir) => {
31288
31579
  const config2 = await readJson(
31289
- await readConfigSourcePath(stateDir),
31580
+ configPath(stateDir),
31290
31581
  DEFAULT_CONFIG
31291
31582
  );
31292
31583
  return {
@@ -31301,28 +31592,28 @@ var writeConfig = async (stateDir, config2) => {
31301
31592
 
31302
31593
  // src/skillIntegration.ts
31303
31594
  import { constants } from "fs";
31304
- import { access as access3 } from "fs/promises";
31595
+ import { access as access2 } from "fs/promises";
31305
31596
  import os from "os";
31306
- import path17 from "path";
31597
+ import path19 from "path";
31307
31598
  import Enquirer from "enquirer";
31308
31599
 
31309
31600
  // src/skillProviders/claude.ts
31310
- import path15 from "path";
31601
+ import path17 from "path";
31311
31602
  var claudeSkillProvider = {
31312
31603
  id: "claude",
31313
- globalRootCandidates: (home) => [path15.join(home, ".claude", "skills")],
31314
- localRoot: (projectDir) => path15.join(projectDir, ".claude", "skills")
31604
+ globalRootCandidates: (home) => [path17.join(home, ".claude", "skills")],
31605
+ localRoot: (projectDir) => path17.join(projectDir, ".claude", "skills")
31315
31606
  };
31316
31607
 
31317
31608
  // src/skillProviders/codex.ts
31318
- import path16 from "path";
31609
+ import path18 from "path";
31319
31610
  var codexSkillProvider = {
31320
31611
  id: "codex",
31321
31612
  globalRootCandidates: (home) => [
31322
- path16.join(home, ".codex", "skills"),
31323
- path16.join(home, ".agents", "skills")
31613
+ path18.join(home, ".codex", "skills"),
31614
+ path18.join(home, ".agents", "skills")
31324
31615
  ],
31325
- localRoot: (projectDir) => path16.join(projectDir, ".codex", "skills")
31616
+ localRoot: (projectDir) => path18.join(projectDir, ".codex", "skills")
31326
31617
  };
31327
31618
 
31328
31619
  // src/skillProviders/registry.ts
@@ -31343,13 +31634,13 @@ var expandHomePath = (input) => {
31343
31634
  return process.env.HOME || os.homedir();
31344
31635
  }
31345
31636
  if (input.startsWith("~/")) {
31346
- return path17.join(process.env.HOME || os.homedir(), input.slice(2));
31637
+ return path19.join(process.env.HOME || os.homedir(), input.slice(2));
31347
31638
  }
31348
31639
  return input;
31349
31640
  };
31350
31641
  var pathExists3 = async (filePath) => {
31351
31642
  try {
31352
- await access3(filePath);
31643
+ await access2(filePath);
31353
31644
  return true;
31354
31645
  } catch {
31355
31646
  return false;
@@ -31860,13 +32151,13 @@ var editEncryptedText = async ({
31860
32151
 
31861
32152
  // src/openInEditor.ts
31862
32153
  import { spawn } from "child_process";
31863
- 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";
31864
32155
  import { tmpdir } from "os";
31865
- import path18 from "path";
32156
+ import path20 from "path";
31866
32157
  var openInEditor = async (currentText, { extension = "" } = {}) => {
31867
32158
  const editor = process.env.EDITOR?.trim() || "vi";
31868
- const dir = await mkdtemp(path18.join(tmpdir(), "fm-edit-"));
31869
- 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}`);
31870
32161
  try {
31871
32162
  await writeFile5(tempPath, currentText, "utf8");
31872
32163
  const exitCode = await new Promise((resolve, reject) => {
@@ -31881,9 +32172,9 @@ var openInEditor = async (currentText, { extension = "" } = {}) => {
31881
32172
  if (exitCode !== 0) {
31882
32173
  return null;
31883
32174
  }
31884
- return await readFile6(tempPath, "utf8");
32175
+ return await readFile7(tempPath, "utf8");
31885
32176
  } finally {
31886
- await rm3(dir, { force: true, recursive: true }).catch(() => {
32177
+ await rm4(dir, { force: true, recursive: true }).catch(() => {
31887
32178
  });
31888
32179
  }
31889
32180
  };
@@ -32025,7 +32316,7 @@ Usage:
32025
32316
  fm push [filePath] [--token <jwt>] [--force] [--yes|-y] [--dry-run|-d] [--verbose|--debug]
32026
32317
  fm status [--token <jwt>] [--json]
32027
32318
  fm s [--token <jwt>] [--json]
32028
- 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]
32029
32320
  fm sync --watch [<path>] [--notify] [--json] [--readonly]
32030
32321
  fm sync --watch <path...> [--paths-file <file>] [--notify] [--json]
32031
32322
  fm sync --dry-run|-d [--token <jwt>] [--json]
@@ -32057,7 +32348,7 @@ Usage:
32057
32348
  fm --version
32058
32349
 
32059
32350
  Store creation:
32060
- 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.
32061
32352
  fm store create <workspace/storeId> creates in that explicitly selected workspace.
32062
32353
 
32063
32354
  Auth:
@@ -32075,7 +32366,7 @@ Output:
32075
32366
  `;
32076
32367
  var readCliVersion = async () => {
32077
32368
  const packageJson = JSON.parse(
32078
- await readFile7(new URL("../package.json", import.meta.url), "utf8")
32369
+ await readFile8(new URL("../package.json", import.meta.url), "utf8")
32079
32370
  );
32080
32371
  return typeof packageJson.version === "string" ? packageJson.version : "unknown";
32081
32372
  };
@@ -32116,6 +32407,7 @@ var parseArgvCommandName = (argv) => {
32116
32407
  var parseArgs = (argv) => {
32117
32408
  const positional = [];
32118
32409
  const shouldParseDirectPullMapping = argv.includes("pull") && argv.includes("--direct");
32410
+ const isSyncCommand = parseArgvCommandName(argv) === "sync";
32119
32411
  const isCatCommand = parseArgvCommandName(argv) === "cat";
32120
32412
  let token = null;
32121
32413
  const storeFilters = [];
@@ -32135,6 +32427,7 @@ var parseArgs = (argv) => {
32135
32427
  let stateDir = defaultStateDir();
32136
32428
  let pollIntervalMs = null;
32137
32429
  let dryRun = false;
32430
+ let conflictPolicy;
32138
32431
  let force = false;
32139
32432
  let direct = false;
32140
32433
  let json2 = false;
@@ -32181,7 +32474,7 @@ var parseArgs = (argv) => {
32181
32474
  continue;
32182
32475
  }
32183
32476
  if (current === "--file" || current === "-f") {
32184
- inputFilePath = next ? path19.resolve(next) : inputFilePath;
32477
+ inputFilePath = next ? path21.resolve(next) : inputFilePath;
32185
32478
  index += 1;
32186
32479
  continue;
32187
32480
  }
@@ -32246,7 +32539,7 @@ var parseArgs = (argv) => {
32246
32539
  if (!next) {
32247
32540
  throw new Error("--target-dir requires a path.");
32248
32541
  }
32249
- skillTargetDir = path19.resolve(expandHomePath(next));
32542
+ skillTargetDir = path21.resolve(expandHomePath(next));
32250
32543
  index += 1;
32251
32544
  continue;
32252
32545
  }
@@ -32258,7 +32551,7 @@ var parseArgs = (argv) => {
32258
32551
  continue;
32259
32552
  }
32260
32553
  if (current === "--state-dir") {
32261
- stateDir = path19.resolve(next ?? stateDir);
32554
+ stateDir = path21.resolve(next ?? stateDir);
32262
32555
  index += 1;
32263
32556
  continue;
32264
32557
  }
@@ -32266,12 +32559,12 @@ var parseArgs = (argv) => {
32266
32559
  if (!next) {
32267
32560
  throw new Error("--paths-file requires a file path.");
32268
32561
  }
32269
- pathsFile = path19.resolve(expandHomePath(next));
32562
+ pathsFile = path21.resolve(expandHomePath(next));
32270
32563
  index += 1;
32271
32564
  continue;
32272
32565
  }
32273
32566
  if (current.startsWith("--paths-file=")) {
32274
- pathsFile = path19.resolve(
32567
+ pathsFile = path21.resolve(
32275
32568
  expandHomePath(current.slice("--paths-file=".length))
32276
32569
  );
32277
32570
  continue;
@@ -32285,6 +32578,15 @@ var parseArgs = (argv) => {
32285
32578
  openBrowser = false;
32286
32579
  continue;
32287
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
+ }
32288
32590
  if (current === "--force") {
32289
32591
  force = true;
32290
32592
  continue;
@@ -32337,6 +32639,7 @@ var parseArgs = (argv) => {
32337
32639
  }
32338
32640
  return {
32339
32641
  args: positional,
32642
+ conflictPolicy,
32340
32643
  token,
32341
32644
  storeFilters,
32342
32645
  searchLimit,
@@ -32459,10 +32762,10 @@ var listDirectoryEntries = async (listDirectory, directoryPath) => {
32459
32762
  if (left.type !== "directory" && right.type === "directory") {
32460
32763
  return 1;
32461
32764
  }
32462
- return path19.basename(left.path).localeCompare(path19.basename(right.path));
32765
+ return path21.basename(left.path).localeCompare(path21.basename(right.path));
32463
32766
  }).forEach((entry) => {
32464
32767
  const suffix = entry.type === "directory" ? "/" : "";
32465
- renderOutput(`${path19.basename(entry.path)}${suffix}`);
32768
+ renderOutput(`${path21.basename(entry.path)}${suffix}`);
32466
32769
  });
32467
32770
  };
32468
32771
  var listStores = async (listStoresMetadata) => {
@@ -32757,7 +33060,7 @@ var normalizeSkillAgents = (inputs) => {
32757
33060
  return [...new Set(inputs.map(normalizeSkillAgent))];
32758
33061
  };
32759
33062
  var normalizeRemoteSkillPath = (input) => {
32760
- const normalized = path19.posix.normalize(`/${input.trim()}`);
33063
+ const normalized = path21.posix.normalize(`/${input.trim()}`);
32761
33064
  if (normalized === "/") {
32762
33065
  throw new Error("Skill path must not be the store root.");
32763
33066
  }
@@ -32767,11 +33070,11 @@ var normalizeRemoteSkillPath = (input) => {
32767
33070
  return normalized;
32768
33071
  };
32769
33072
  var parentRemotePath = (filePath) => {
32770
- const parent = path19.posix.dirname(filePath);
33073
+ const parent = path21.posix.dirname(filePath);
32771
33074
  return parent === "." ? "/" : parent;
32772
33075
  };
32773
33076
  var joinRemotePath = (left, right) => {
32774
- return path19.posix.join(left, right).replace(/\/+/g, "/");
33077
+ return path21.posix.join(left, right).replace(/\/+/g, "/");
32775
33078
  };
32776
33079
  var readRemoteSkillTextFile = async (filePath, storeRef, client) => {
32777
33080
  const file2 = await client.readFileByRef({ ...storeRef, path: filePath });
@@ -32833,7 +33136,7 @@ var collectRemoteSkillFiles = async ({
32833
33136
  `Skill contains unsupported ${entry.type} entry: ${entry.path}`
32834
33137
  );
32835
33138
  }
32836
- const relativePath = path19.posix.relative(rootPath, entry.path);
33139
+ const relativePath = path21.posix.relative(rootPath, entry.path);
32837
33140
  if (!relativePath || relativePath.startsWith("..")) {
32838
33141
  throw new Error(
32839
33142
  `Unexpected skill entry outside skill root: ${entry.path}`
@@ -32847,7 +33150,7 @@ var collectRemoteSkillFiles = async ({
32847
33150
  };
32848
33151
  var resolveRemoteSkillRoot = async (inputPath, storeRef, client) => {
32849
33152
  const normalizedPath = normalizeRemoteSkillPath(inputPath);
32850
- if (path19.posix.basename(normalizedPath) === "SKILL.md") {
33153
+ if (path21.posix.basename(normalizedPath) === "SKILL.md") {
32851
33154
  await readRemoteSkillTextFile(normalizedPath, storeRef, client);
32852
33155
  return {
32853
33156
  rootPath: parentRemotePath(normalizedPath),
@@ -32927,7 +33230,7 @@ var resolveSkillInstallTargets = async ({
32927
33230
  return {
32928
33231
  agent,
32929
33232
  rootDir,
32930
- targetDir: path19.join(rootDir, skillName)
33233
+ targetDir: path21.join(rootDir, skillName)
32931
33234
  };
32932
33235
  })
32933
33236
  );
@@ -32966,24 +33269,24 @@ var formatSkillTarget = (target, existingTargetDirs) => {
32966
33269
  return ` ${target.agent.padEnd(7)} ${target.targetDir}${suffix}`;
32967
33270
  };
32968
33271
  var writeSkillFiles = async (target, files, replaceExisting) => {
32969
- const temporaryDirectory = path19.join(
33272
+ const temporaryDirectory = path21.join(
32970
33273
  target.rootDir,
32971
- `.${path19.basename(target.targetDir)}.tmp-${process.pid}-${Date.now()}`
33274
+ `.${path21.basename(target.targetDir)}.tmp-${process.pid}-${Date.now()}`
32972
33275
  );
32973
- await rm4(temporaryDirectory, { recursive: true, force: true });
32974
- await mkdir5(temporaryDirectory, { recursive: true });
33276
+ await rm5(temporaryDirectory, { recursive: true, force: true });
33277
+ await mkdir6(temporaryDirectory, { recursive: true });
32975
33278
  try {
32976
33279
  for (const file2 of files) {
32977
- const localPath = path19.join(temporaryDirectory, file2.relativePath);
32978
- await mkdir5(path19.dirname(localPath), { recursive: true });
33280
+ const localPath = path21.join(temporaryDirectory, file2.relativePath);
33281
+ await mkdir6(path21.dirname(localPath), { recursive: true });
32979
33282
  await writeFile6(localPath, file2.bytes);
32980
33283
  }
32981
33284
  if (replaceExisting) {
32982
- await rm4(target.targetDir, { recursive: true, force: true });
33285
+ await rm5(target.targetDir, { recursive: true, force: true });
32983
33286
  }
32984
33287
  await rename4(temporaryDirectory, target.targetDir);
32985
33288
  } catch (error51) {
32986
- await rm4(temporaryDirectory, { recursive: true, force: true });
33289
+ await rm5(temporaryDirectory, { recursive: true, force: true });
32987
33290
  throw error51;
32988
33291
  }
32989
33292
  };
@@ -33026,7 +33329,7 @@ var installSkillBundle = async ({
33026
33329
  throw new Error("Aborted skill install.");
33027
33330
  }
33028
33331
  for (const target of targets) {
33029
- await mkdir5(target.rootDir, { recursive: true });
33332
+ await mkdir6(target.rootDir, { recursive: true });
33030
33333
  }
33031
33334
  for (const target of targets) {
33032
33335
  await writeSkillFiles(
@@ -33058,7 +33361,7 @@ var readRemoteSkillBundle = async ({
33058
33361
  client
33059
33362
  );
33060
33363
  const metadata = parseSkillMetadata(skillMarkdown);
33061
- const rootName = path19.posix.basename(rootPath);
33364
+ const rootName = path21.posix.basename(rootPath);
33062
33365
  if (metadata.name !== rootName) {
33063
33366
  throw new Error(
33064
33367
  `SKILL.md name "${metadata.name}" must match skill directory "${rootName}".`
@@ -33238,7 +33541,7 @@ var readWriteTextInput = async (inlineText, inputFilePath) => {
33238
33541
  throw new Error("Provide either inline text or --file, not both.");
33239
33542
  }
33240
33543
  if (inputFilePath) {
33241
- return readFile7(inputFilePath, "utf8");
33544
+ return readFile8(inputFilePath, "utf8");
33242
33545
  }
33243
33546
  return inlineText ?? null;
33244
33547
  };
@@ -33254,7 +33557,7 @@ var readRemoteTextFile = async (filePath, storeRef, client) => {
33254
33557
  return text;
33255
33558
  };
33256
33559
  var normalizeRemotePath = (input) => {
33257
- const normalized = path19.posix.normalize(`/${input.trim()}`);
33560
+ const normalized = path21.posix.normalize(`/${input.trim()}`);
33258
33561
  if (normalized.includes("\0")) {
33259
33562
  throw new Error("Path contains an invalid null byte.");
33260
33563
  }
@@ -33548,7 +33851,7 @@ var diffRemoteFiles = async ({
33548
33851
  storeRef
33549
33852
  }) => {
33550
33853
  const leftText = await readRemoteTextFile(leftPath, storeRef, client);
33551
- 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);
33552
33855
  return buildUnifiedDiff({
33553
33856
  leftLabel: leftPath,
33554
33857
  leftText,
@@ -33615,8 +33918,8 @@ var copyRemotePath = async (args, storeRef, client) => {
33615
33918
  (entry) => entry.path.startsWith(`${normalizedSource}/`)
33616
33919
  );
33617
33920
  for (const entry of descendants) {
33618
- const relativePath = path19.posix.relative(normalizedSource, entry.path);
33619
- const targetPath = path19.posix.join(normalizedDestination, relativePath);
33921
+ const relativePath = path21.posix.relative(normalizedSource, entry.path);
33922
+ const targetPath = path21.posix.join(normalizedDestination, relativePath);
33620
33923
  if (entry.type === "directory") {
33621
33924
  await createRemoteDirectory(targetPath, storeRef, client);
33622
33925
  continue;
@@ -33811,19 +34114,19 @@ var runDecryptCommand = async (rest, parsed, client) => {
33811
34114
  );
33812
34115
  };
33813
34116
  var runEditCommand = async (rest, parsed, client) => {
33814
- const filePath = parsed.inputFilePath ?? (rest[0] ? path19.resolve(rest[0]) : null);
34117
+ const filePath = parsed.inputFilePath ?? (rest[0] ? path21.resolve(rest[0]) : null);
33815
34118
  if (!filePath) {
33816
34119
  throw new Error(
33817
34120
  `Usage: ${commandNames} edit (<localPath>|--file|-f <localPath>) [--password <password>] [--token <jwt>]`
33818
34121
  );
33819
34122
  }
33820
- const encryptedText = await readFile7(filePath, "utf8");
34123
+ const encryptedText = await readFile8(filePath, "utf8");
33821
34124
  const [password, userEncryption] = await Promise.all([
33822
34125
  readEncryptionPassword(parsed),
33823
34126
  readUserEncryption(client)
33824
34127
  ]);
33825
34128
  const result = await editEncryptedText({
33826
- edit: (currentPlaintext) => openInEditor(currentPlaintext, { extension: path19.extname(filePath) }),
34129
+ edit: (currentPlaintext) => openInEditor(currentPlaintext, { extension: path21.extname(filePath) }),
33827
34130
  encryptedText,
33828
34131
  password,
33829
34132
  userEncryption: userEncryption.userEncryption
@@ -33899,10 +34202,10 @@ var runEncryptAddRecipientCommand = async (rest, parsed, client) => {
33899
34202
  `);
33900
34203
  renderOutput(result.encryptedText);
33901
34204
  };
33902
- var defaultEncryptionMigrationBackupPath = () => path19.resolve(process.cwd(), "doctrine-encryption-migration.json");
33903
- 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"));
33904
34207
  var writeEncryptionMigrationBackup = async (backupPath, backup) => {
33905
- await mkdir5(path19.dirname(backupPath), { recursive: true });
34208
+ await mkdir6(path21.dirname(backupPath), { recursive: true });
33906
34209
  await writeFile6(backupPath, `${JSON.stringify(backup, null, 2)}
33907
34210
  `, "utf8");
33908
34211
  };
@@ -34190,7 +34493,7 @@ var runEncryptionMigrateCommand = async (rest, parsed, client) => {
34190
34493
  `Usage: ${commandNames} encryption migrate [backup-file] [--store <storeId>] [--password <password>] [--dry-run] [--yes]`
34191
34494
  );
34192
34495
  }
34193
- const backupPath = path19.resolve(
34496
+ const backupPath = path21.resolve(
34194
34497
  rest[1] ?? defaultEncryptionMigrationBackupPath()
34195
34498
  );
34196
34499
  const password = await readEncryptionPassword(parsed);
@@ -34265,7 +34568,7 @@ var doesRemoteQueryMatchPath = (query, filePath) => {
34265
34568
  if (query.startsWith("/")) {
34266
34569
  return normalizedPath === query;
34267
34570
  }
34268
- const filename = path19.basename(filePath).toLowerCase();
34571
+ const filename = path21.basename(filePath).toLowerCase();
34269
34572
  return filename === query || normalizedPath === query || filename.includes(query) || normalizedPath.includes(query);
34270
34573
  };
34271
34574
  var queryRemoteFiles = async (query, storeFilters, client) => {
@@ -34337,7 +34640,7 @@ var printRemoteQueryMatches = (matches) => {
34337
34640
  });
34338
34641
  };
34339
34642
  var normalizeRemoteDirectoryPath = (input) => {
34340
- const normalized = path19.posix.normalize(`/${input.trim()}`);
34643
+ const normalized = path21.posix.normalize(`/${input.trim()}`);
34341
34644
  if (normalized.includes("\0")) {
34342
34645
  throw new Error("Path contains an invalid null byte.");
34343
34646
  }
@@ -34457,12 +34760,12 @@ var labelFromTreeDirectoryPath = (directoryPath) => {
34457
34760
  if (directoryPath === "/") {
34458
34761
  return "./";
34459
34762
  }
34460
- return `${path19.posix.basename(directoryPath)}/`;
34763
+ return `${path21.posix.basename(directoryPath)}/`;
34461
34764
  };
34462
34765
  var doesTreeFilterMatchDirectory = (filter, directoryPath) => {
34463
34766
  const normalizedFilter = filter.trim().toLowerCase();
34464
34767
  const normalizedPath = directoryPath.toLowerCase();
34465
- const basename3 = path19.posix.basename(directoryPath).toLowerCase();
34768
+ const basename3 = path21.posix.basename(directoryPath).toLowerCase();
34466
34769
  return basename3.includes(normalizedFilter) || normalizedPath.includes(normalizedFilter);
34467
34770
  };
34468
34771
  var resolveTreeDirectoryPath = async ({
@@ -34517,7 +34820,7 @@ var printRemoteTreeChildren = async ({
34517
34820
  const childPrefix = isLast ? " " : "\u2502 ";
34518
34821
  const suffix = entry.type === "directory" ? "/" : "";
34519
34822
  lines.push(
34520
- `${prefix}${connector}${path19.posix.basename(entry.path)}${suffix}`
34823
+ `${prefix}${connector}${path21.posix.basename(entry.path)}${suffix}`
34521
34824
  );
34522
34825
  if (entry.type === "directory") {
34523
34826
  await printRemoteTreeChildren({
@@ -34856,7 +35159,7 @@ var parseRootStorePath = (storePathInput) => {
34856
35159
  var parseNewStorePath = (storePathInput) => {
34857
35160
  return parseRootStorePath(storePathInput);
34858
35161
  };
34859
- 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.";
34860
35163
  var parseStoreCreateInput = async (storePathInput) => {
34861
35164
  const segments2 = storePathInput.split("/").map((segment) => segment.trim()).filter(Boolean);
34862
35165
  if (segments2.length === 2) {
@@ -35129,9 +35432,9 @@ var shouldPrintCheckoutDetail = (detail, verbose) => {
35129
35432
  var formatCheckoutDetail = (detail, options2) => {
35130
35433
  const status = checkoutStatusLabel(detail, options2);
35131
35434
  const direction = checkoutDirectionLabel(detail);
35132
- const path20 = formatCheckoutDetailPath(detail, options2.syncRules);
35435
+ const path22 = formatCheckoutDetailPath(detail, options2.syncRules);
35133
35436
  const reason = styleText.dim(checkoutDetailMessage(detail));
35134
- return direction ? `${direction} ${status} ${path20} ${reason}` : `${status} ${path20} ${reason}`;
35437
+ return direction ? `${direction} ${status} ${path22} ${reason}` : `${status} ${path22} ${reason}`;
35135
35438
  };
35136
35439
  var formatCheckoutDetailPath = (detail, syncRules) => {
35137
35440
  const localPath = formatLocalCheckoutPath(detail.relativePath);
@@ -35343,9 +35646,9 @@ var checkoutReportGroups = (details, options2) => {
35343
35646
  );
35344
35647
  };
35345
35648
  var checkoutReportDetailLine = (detail, options2) => {
35346
- const path20 = formatCheckoutDetailPath(detail, options2.syncRules);
35649
+ const path22 = formatCheckoutDetailPath(detail, options2.syncRules);
35347
35650
  const message = detail.status === "failed" || detail.status === "skipped" || detail.status === "ignored" ? detail.message ?? checkoutDetailMessage(detail) : "";
35348
- return message ? ` ${path20} ${styleText.dim(message)}` : ` ${path20}`;
35651
+ return message ? ` ${path22} ${styleText.dim(message)}` : ` ${path22}`;
35349
35652
  };
35350
35653
  var checkoutReportGroupBlock = ({
35351
35654
  dryRun,
@@ -35472,13 +35775,13 @@ var statusChangeKind = (detail) => {
35472
35775
  var formatStatusChange = (detail, syncRules) => {
35473
35776
  const kind = statusChangeKind(detail);
35474
35777
  const tone = kind === "deleted" ? "error" : kind === "added" ? "ok" : "warn";
35475
- const path20 = formatCheckoutDetailPath(detail, syncRules);
35476
- return ` ${styleText.color(tone, kind.padEnd(8))} ${path20}`;
35778
+ const path22 = formatCheckoutDetailPath(detail, syncRules);
35779
+ return ` ${styleText.color(tone, kind.padEnd(8))} ${path22}`;
35477
35780
  };
35478
35781
  var formatStatusBlock = (detail, syncRules) => {
35479
35782
  const message = detail.message ? ` ${styleText.dim(detail.message)}` : "";
35480
- const path20 = formatCheckoutDetailPath(detail, syncRules);
35481
- 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}`;
35482
35785
  };
35483
35786
  var actionableStatusDetails = (result) => result.details.filter(
35484
35787
  (detail) => detail.status === "succeeded" || detail.status === "deleted"
@@ -35611,7 +35914,7 @@ var assertCheckoutSyncEnabled = (checkout) => {
35611
35914
  return;
35612
35915
  }
35613
35916
  throw new Error(
35614
- "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."
35615
35918
  );
35616
35919
  };
35617
35920
  var warnReadonlyCheckoutPush = (checkout, force) => {
@@ -35619,7 +35922,7 @@ var warnReadonlyCheckoutPush = (checkout, force) => {
35619
35922
  return;
35620
35923
  }
35621
35924
  renderWarning(
35622
- "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."
35623
35926
  );
35624
35927
  };
35625
35928
  var referenceStructuralChangeLabel = (detail) => {
@@ -35678,20 +35981,26 @@ var confirmCheckoutReferenceStructuralWrites = async ({
35678
35981
  yes
35679
35982
  }) => {
35680
35983
  const result = operation === "sync" ? await syncCheckout({
35984
+ ignoreSettings: "sync",
35681
35985
  client,
35682
35986
  dryRun: true,
35683
35987
  force,
35684
35988
  ignorePatterns: checkout.ignore,
35989
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
35990
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35685
35991
  localPath: checkout.rootDir,
35686
35992
  progress,
35687
35993
  storeRef: {},
35688
35994
  syncRules: checkout.syncRules
35689
35995
  }) : await pushCheckout({
35996
+ ignoreSettings: "preview",
35690
35997
  client,
35691
35998
  dryRun: true,
35692
35999
  filePaths,
35693
36000
  force,
35694
36001
  ignorePatterns: checkout.ignore,
36002
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36003
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35695
36004
  localPath: checkout.rootDir,
35696
36005
  progress,
35697
36006
  storeRef: {},
@@ -35736,12 +36045,18 @@ var runCheckoutOperation = async ({
35736
36045
  filePaths,
35737
36046
  force,
35738
36047
  ignorePatterns: checkout.ignore,
36048
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36049
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35739
36050
  localPath: checkout.rootDir,
35740
36051
  progress,
35741
36052
  storeRef: {},
35742
36053
  syncRules: checkout.syncRules
35743
36054
  };
35744
- 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
+ });
35745
36060
  formatCheckoutResult(labels.summaryVerb, result, {
35746
36061
  detailsRendered: Boolean(progress),
35747
36062
  dryRun,
@@ -35835,6 +36150,7 @@ var assertCheckoutReadyForReadOnlyPull = async (checkout) => {
35835
36150
  };
35836
36151
  var runCheckoutPullThenPush = async ({
35837
36152
  checkout,
36153
+ conflictPolicy,
35838
36154
  client,
35839
36155
  force,
35840
36156
  verbose,
@@ -35852,6 +36168,8 @@ var runCheckoutPullThenPush = async ({
35852
36168
  }
35853
36169
  progress?.start?.();
35854
36170
  const result = await syncCheckout({
36171
+ ignoreSettings: "sync",
36172
+ conflictPolicy,
35855
36173
  beforeRuleApply: async ({ result: rulePlan }) => {
35856
36174
  await confirmReferenceStructuralWrites(
35857
36175
  referenceStructuralWriteDetails(rulePlan),
@@ -35861,6 +36179,8 @@ var runCheckoutPullThenPush = async ({
35861
36179
  client,
35862
36180
  force,
35863
36181
  ignorePatterns: checkout.ignore,
36182
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36183
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35864
36184
  localPath: checkout.rootDir,
35865
36185
  progress,
35866
36186
  storeRef: {},
@@ -35887,10 +36207,13 @@ var assertGitSyncHasNoPushPlan = async ({
35887
36207
  force
35888
36208
  }) => {
35889
36209
  const result = await pushCheckout({
36210
+ ignoreSettings: "preview",
35890
36211
  client,
35891
36212
  dryRun: true,
35892
36213
  force,
35893
36214
  ignorePatterns: checkout.ignore,
36215
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36216
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
35894
36217
  localPath: checkout.rootDir,
35895
36218
  storeRef: {},
35896
36219
  syncRules: checkout.syncRules
@@ -35914,16 +36237,6 @@ var assertGitSyncHasNoPushPlan = async ({
35914
36237
  ].join("\n")
35915
36238
  );
35916
36239
  };
35917
- var refreshStoreIgnoreCache = async (checkout, client) => {
35918
- const storeRef = await resolveProjectStoreRef();
35919
- if (!storeRef) {
35920
- return null;
35921
- }
35922
- const metadata = await client.readStoreByRef(storeRef);
35923
- const storeIgnore = metadata?.ignore ?? [];
35924
- await writeStoreIgnoreCache(checkout.rootDir, storeIgnore);
35925
- return { storeIgnore, storeRef };
35926
- };
35927
36240
  var analyzeLocalIgnore = async (checkout) => {
35928
36241
  const [files, presentMarkers, gitignorePatterns] = await Promise.all([
35929
36242
  collectLocalCheckoutFiles(checkout.rootDir, []),
@@ -35954,17 +36267,30 @@ var promptIgnoreAction = async () => {
35954
36267
  };
35955
36268
  var isInteractiveTty = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
35956
36269
  var runIgnoreSetupGate = async (checkout, client, assumeYes) => {
35957
- const refreshed = await refreshStoreIgnoreCache(checkout, client);
35958
- if (!refreshed) {
35959
- return;
35960
- }
35961
- const { storeIgnore, storeRef } = refreshed;
35962
- if (storeIgnore.length > 0) {
35963
- 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;
35964
36289
  }
36290
+ if (storeIgnore.length > 0) return;
35965
36291
  await maybePromptIgnoreSetup({
35966
36292
  assumeYes,
35967
- client,
36293
+ writePatterns: (patterns) => writeSharedIgnoreFile(sharedFilePath, patterns),
35968
36294
  analysis: await analyzeLocalIgnore(checkout),
35969
36295
  onboarded: await readIgnoreOnboardedAt(checkout.rootDir) !== null,
35970
36296
  currentPatterns: storeIgnore,
@@ -35972,11 +36298,11 @@ var runIgnoreSetupGate = async (checkout, client, assumeYes) => {
35972
36298
  chooseAction: promptIgnoreAction,
35973
36299
  markOnboarded: () => markIgnoreOnboarded(checkout.rootDir),
35974
36300
  renderOutput,
35975
- renderWarning,
35976
- storeRef
36301
+ renderWarning
35977
36302
  });
35978
36303
  };
35979
36304
  var runCheckoutSyncOnce = async ({
36305
+ conflictPolicy,
35980
36306
  client,
35981
36307
  force,
35982
36308
  verbose,
@@ -35984,17 +36310,27 @@ var runCheckoutSyncOnce = async ({
35984
36310
  }) => {
35985
36311
  const checkout = await resolveCheckoutConfig();
35986
36312
  assertCheckoutSyncEnabled(checkout);
35987
- await runCheckoutPullThenPush({ checkout, client, force, verbose, yes });
36313
+ await runCheckoutPullThenPush({
36314
+ checkout,
36315
+ client,
36316
+ conflictPolicy,
36317
+ force,
36318
+ verbose,
36319
+ yes
36320
+ });
35988
36321
  };
35989
36322
  var DEFAULT_FULL_SYNC_INTERVAL_MS = 5 * 60 * 1e3;
35990
36323
  var runCheckoutSyncCollecting = async ({
35991
36324
  checkout,
36325
+ conflictPolicy,
35992
36326
  client,
35993
36327
  force,
35994
36328
  yes = false
35995
36329
  }) => {
35996
36330
  assertCheckoutSyncEnabled(checkout);
35997
36331
  return syncCheckout({
36332
+ ignoreSettings: "sync",
36333
+ conflictPolicy,
35998
36334
  beforeRuleApply: async ({ result }) => {
35999
36335
  await confirmReferenceStructuralWrites(
36000
36336
  referenceStructuralWriteDetails(result),
@@ -36004,6 +36340,8 @@ var runCheckoutSyncCollecting = async ({
36004
36340
  client,
36005
36341
  force,
36006
36342
  ignorePatterns: checkout.ignore,
36343
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36344
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
36007
36345
  localPath: checkout.rootDir,
36008
36346
  storeRef: {},
36009
36347
  syncRules: checkout.syncRules
@@ -36030,14 +36368,15 @@ var runCheckoutStatus = async ({
36030
36368
  }) => {
36031
36369
  const initialCheckout = await resolveCheckoutConfig();
36032
36370
  assertCheckoutSyncEnabled(initialCheckout);
36033
- await refreshStoreIgnoreCache(initialCheckout, client);
36034
- const checkout = await resolveCheckoutConfig();
36035
- assertCheckoutSyncEnabled(checkout);
36371
+ const checkout = initialCheckout;
36036
36372
  const pullResult = await pullCheckout({
36373
+ ignoreSettings: "preview",
36037
36374
  client,
36038
36375
  dryRun: true,
36039
36376
  force,
36040
36377
  ignorePatterns: checkout.ignore,
36378
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36379
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
36041
36380
  localPath: checkout.rootDir,
36042
36381
  storeRef: {},
36043
36382
  syncRules: checkout.syncRules
@@ -36080,10 +36419,13 @@ var runCheckoutStatus = async ({
36080
36419
  return;
36081
36420
  }
36082
36421
  const pushResult = await pushCheckout({
36422
+ ignoreSettings: "preview",
36083
36423
  client,
36084
36424
  dryRun: true,
36085
36425
  force,
36086
36426
  ignorePatterns: checkout.ignore,
36427
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
36428
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
36087
36429
  localPath: checkout.rootDir,
36088
36430
  storeRef: {},
36089
36431
  syncRules: checkout.syncRules
@@ -36096,18 +36438,18 @@ var runCheckoutStatus = async ({
36096
36438
  });
36097
36439
  };
36098
36440
  var isCheckoutStateGitPath = isCheckoutStateDirectoryPath;
36099
- var checkoutFilesManifestPath2 = (checkoutRootDir) => `${resolveCheckoutStateDirectoryName(checkoutRootDir)}/files.json`;
36441
+ var checkoutFilesManifestPath2 = () => `${resolveCheckoutStateDirectoryName()}/files.json`;
36100
36442
  var isInsideOrEqual = (parentPath, childPath2) => {
36101
- const relativePath = path19.relative(parentPath, childPath2);
36102
- return relativePath === "" || !relativePath.startsWith("..") && !path19.isAbsolute(relativePath);
36443
+ const relativePath = path21.relative(parentPath, childPath2);
36444
+ return relativePath === "" || !relativePath.startsWith("..") && !path21.isAbsolute(relativePath);
36103
36445
  };
36104
36446
  var resolveSyncTargetPaths = (checkout) => checkout.syncRules.map(
36105
- (rule) => path19.resolve(checkout.rootDir, rule.target || ".")
36447
+ (rule) => path21.resolve(checkout.rootDir, rule.target || ".")
36106
36448
  );
36107
- var resolveCheckoutManifestGitPath = (checkout, gitRoot) => path19.relative(
36449
+ var resolveCheckoutManifestGitPath = (checkout, gitRoot) => path21.relative(
36108
36450
  gitRoot,
36109
- path19.join(checkout.rootDir, checkoutFilesManifestPath2(checkout.rootDir))
36110
- ) || checkoutFilesManifestPath2(checkout.rootDir);
36451
+ path21.join(checkout.rootDir, checkoutFilesManifestPath2())
36452
+ ) || checkoutFilesManifestPath2();
36111
36453
  var stageCheckoutManifest = async ({
36112
36454
  checkout,
36113
36455
  gitRoot
@@ -36132,7 +36474,7 @@ var assertGitChangesStayInSyncTargets = async ({
36132
36474
  if (isCheckoutStateGitPath(entry.path)) {
36133
36475
  return false;
36134
36476
  }
36135
- const absolutePath = path19.resolve(gitRoot, entry.path);
36477
+ const absolutePath = path21.resolve(gitRoot, entry.path);
36136
36478
  return !allowedPaths.some(
36137
36479
  (allowedPath) => isInsideOrEqual(allowedPath, absolutePath)
36138
36480
  );
@@ -36154,7 +36496,7 @@ var stageGitSyncTargets = async ({
36154
36496
  }) => {
36155
36497
  const targetPaths = [];
36156
36498
  for (const targetPath of resolveSyncTargetPaths(checkout)) {
36157
- const relativePath = path19.relative(gitRoot, targetPath) || ".";
36499
+ const relativePath = path21.relative(gitRoot, targetPath) || ".";
36158
36500
  const tracked = await runGit(gitRoot, ["ls-files", "--", relativePath]);
36159
36501
  if (await pathExists3(targetPath) || tracked.stdout.trim()) {
36160
36502
  targetPaths.push(relativePath);
@@ -36196,7 +36538,7 @@ var runGitSyncCommand = async ({
36196
36538
  assertCheckoutSyncEnabled(checkout);
36197
36539
  if (!checkout.gitSyncCommitMessage) {
36198
36540
  throw new Error(
36199
- "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."
36200
36542
  );
36201
36543
  }
36202
36544
  const gitRoot = await resolveGitRoot(checkout.rootDir);
@@ -36229,6 +36571,7 @@ var runCheckoutReadOnlyPullOnce = async ({
36229
36571
  };
36230
36572
  var buildCheckoutWatchEntry = ({
36231
36573
  checkout,
36574
+ conflictPolicy,
36232
36575
  client,
36233
36576
  force,
36234
36577
  yes
@@ -36237,10 +36580,17 @@ var buildCheckoutWatchEntry = ({
36237
36580
  rootDir: checkout.rootDir,
36238
36581
  startMessage: `Watching ${checkout.rootDir}`,
36239
36582
  storeRefs: changesFeedStoreRefsForCheckout(checkout),
36240
- 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
+ })
36241
36590
  });
36242
36591
  var resolveExplicitCheckoutEntry = async ({
36243
36592
  checkoutPath,
36593
+ conflictPolicy,
36244
36594
  client,
36245
36595
  force,
36246
36596
  yes
@@ -36248,13 +36598,20 @@ var resolveExplicitCheckoutEntry = async ({
36248
36598
  const checkout = await resolveCheckoutConfig(checkoutPath);
36249
36599
  if (checkout.rootDir !== checkoutPath) {
36250
36600
  throw new Error(
36251
- `${checkoutPath} is not a Forum checkout (no forum.yaml here).`
36601
+ `${checkoutPath} is not a Forum checkout (no .forum/sync.yaml here).`
36252
36602
  );
36253
36603
  }
36254
36604
  assertCheckoutSyncEnabled(checkout);
36255
- return buildCheckoutWatchEntry({ checkout, client, force, yes });
36605
+ return buildCheckoutWatchEntry({
36606
+ checkout,
36607
+ client,
36608
+ conflictPolicy,
36609
+ force,
36610
+ yes
36611
+ });
36256
36612
  };
36257
36613
  var resolveDiscoveredCheckoutEntries = async ({
36614
+ conflictPolicy,
36258
36615
  client,
36259
36616
  force,
36260
36617
  rootPath,
@@ -36270,7 +36627,9 @@ var resolveDiscoveredCheckoutEntries = async ({
36270
36627
  if (checkout.syncRules.length === 0) {
36271
36628
  continue;
36272
36629
  }
36273
- entries.push(buildCheckoutWatchEntry({ checkout, client, force, yes }));
36630
+ entries.push(
36631
+ buildCheckoutWatchEntry({ checkout, client, conflictPolicy, force, yes })
36632
+ );
36274
36633
  }
36275
36634
  if (entries.length === 0) {
36276
36635
  throw new Error(
@@ -36280,6 +36639,7 @@ var resolveDiscoveredCheckoutEntries = async ({
36280
36639
  return entries;
36281
36640
  };
36282
36641
  var runCheckoutSyncWatch = async ({
36642
+ conflictPolicy,
36283
36643
  client,
36284
36644
  force,
36285
36645
  intervalMs,
@@ -36352,12 +36712,30 @@ var runCheckoutSyncWatch = async ({
36352
36712
  { watchPaths, pathsFile },
36353
36713
  {
36354
36714
  readPathsFile,
36355
- resolveDiscoveredEntries: (rootPath) => resolveDiscoveredCheckoutEntries({ client, force, rootPath, yes }),
36356
- 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
+ }),
36357
36729
  resolveSingleCheckoutEntry: async () => {
36358
36730
  const checkout = await resolveCheckoutConfig();
36359
36731
  assertCheckoutSyncEnabled(checkout);
36360
- return buildCheckoutWatchEntry({ checkout, client, force, yes });
36732
+ return buildCheckoutWatchEntry({
36733
+ checkout,
36734
+ client,
36735
+ conflictPolicy,
36736
+ force,
36737
+ yes
36738
+ });
36361
36739
  }
36362
36740
  }
36363
36741
  );
@@ -36414,11 +36792,11 @@ var resolveReadableStore = async (storeInput, client) => {
36414
36792
  var writeCheckoutConfig = async (directoryPath, metadata, workspaceSlug, options2 = {}) => {
36415
36793
  if (!workspaceSlug) {
36416
36794
  throw new Error(
36417
- `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.`
36418
36796
  );
36419
36797
  }
36420
36798
  await writeForumProjectConfig(
36421
- path19.join(directoryPath, defaultProjectConfigFilename),
36799
+ path21.join(directoryPath, defaultProjectConfigFilename),
36422
36800
  {
36423
36801
  syncRules: [
36424
36802
  {
@@ -36434,6 +36812,18 @@ var writeCheckoutConfig = async (directoryPath, metadata, workspaceSlug, options
36434
36812
  ]
36435
36813
  }
36436
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
+ });
36437
36827
  };
36438
36828
  var normalizeProjectConfigPath = (value, fieldName, allowCurrentDirectory = false) => {
36439
36829
  const trimmed = value.trim().replace(/^\/+/, "").replace(/^\.\/+/, "").replace(/\/+$/, "");
@@ -36443,7 +36833,7 @@ var normalizeProjectConfigPath = (value, fieldName, allowCurrentDirectory = fals
36443
36833
  }
36444
36834
  throw new Error(`${fieldName} must be a non-empty relative path.`);
36445
36835
  }
36446
- if (path19.isAbsolute(value)) {
36836
+ if (path21.isAbsolute(value)) {
36447
36837
  throw new Error(`${fieldName} must be a relative path.`);
36448
36838
  }
36449
36839
  const segments2 = trimmed.split("/");
@@ -36496,7 +36886,7 @@ var runProjectConfigSyncCommand = async (mode, args) => {
36496
36886
  const projectConfig = await findForumProjectConfig();
36497
36887
  if (!projectConfig) {
36498
36888
  throw new Error(
36499
- "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."
36500
36890
  );
36501
36891
  }
36502
36892
  if (mode === "clear") {
@@ -36599,7 +36989,7 @@ var runProjectConfigSyncCommand = async (mode, args) => {
36599
36989
  var runProjectConfigValidateCommand = async () => {
36600
36990
  const projectConfig = await findForumProjectConfig();
36601
36991
  if (!projectConfig) {
36602
- throw new Error("No forum.yaml found.");
36992
+ throw new Error("No .forum/sync.yaml found.");
36603
36993
  }
36604
36994
  renderOutput(`Valid Forum config: ${projectConfig.path}`);
36605
36995
  renderOutput(
@@ -36609,7 +36999,7 @@ var runProjectConfigValidateCommand = async () => {
36609
36999
  var runProjectConfigStandardizeCommand = async () => {
36610
37000
  const projectConfig = await findForumProjectConfig();
36611
37001
  if (!projectConfig) {
36612
- throw new Error("No forum.yaml found.");
37002
+ throw new Error("No .forum/sync.yaml found.");
36613
37003
  }
36614
37004
  await writeForumProjectConfig(projectConfig.path, {
36615
37005
  defaultStore: projectConfig.defaultStore,
@@ -36625,7 +37015,7 @@ var defaultCloneDirectoryName = (storeId) => {
36625
37015
  var fsErrorCode = (error51) => error51 instanceof Error && "code" in error51 ? String(error51.code ?? "") : "";
36626
37016
  var ensureGitMetadataExists = async (directoryPath) => {
36627
37017
  try {
36628
- await lstat2(path19.join(directoryPath, ".git"));
37018
+ await lstat2(path21.join(directoryPath, ".git"));
36629
37019
  } catch (error51) {
36630
37020
  if (fsErrorCode(error51) === "ENOENT") {
36631
37021
  throw new Error(
@@ -36663,8 +37053,8 @@ var confirmGitMetadataDeletion = async (localPath, assumeYes) => {
36663
37053
  "Refusing to delete .git without interactive confirmation. Rerun with --yes to confirm in a non-interactive shell."
36664
37054
  );
36665
37055
  }
36666
- const gitPath = path19.join(localPath, ".git");
36667
- const gitignorePath = path19.join(localPath, ".gitignore");
37056
+ const gitPath = path21.join(localPath, ".git");
37057
+ const gitignorePath = path21.join(localPath, ".gitignore");
36668
37058
  const confirmationPhrase = "DELETE GIT";
36669
37059
  renderWarning("");
36670
37060
  renderWarning("DANGER: Forum is about to remove Git from this checkout.");
@@ -36706,11 +37096,11 @@ var runGitConvertCommand = async ({
36706
37096
  const { created, metadata, workspaceSlug, storeRef } = await resolveOrCreateConversionStore(storeInput, client);
36707
37097
  if (!workspaceSlug) {
36708
37098
  throw new Error(
36709
- `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.`
36710
37100
  );
36711
37101
  }
36712
37102
  await writeForumProjectConfig(
36713
- path19.join(localPath, defaultProjectConfigFilename),
37103
+ path21.join(localPath, defaultProjectConfigFilename),
36714
37104
  {
36715
37105
  syncRules: [
36716
37106
  {
@@ -36725,14 +37115,24 @@ var runGitConvertCommand = async ({
36725
37115
  ]
36726
37116
  }
36727
37117
  );
36728
- if (ignorePatterns.length > 0) {
36729
- await client.upsertStoreByRef({
36730
- ...storeRef,
36731
- patch: { ignore: ignorePatterns }
36732
- });
36733
- }
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
+ );
36734
37133
  const conversionIgnorePatterns = [...ignorePatterns, ".gitignore"];
36735
37134
  await pushCheckout({
37135
+ ignoreSettings: "preview",
36736
37136
  client,
36737
37137
  dryRun: true,
36738
37138
  force,
@@ -36741,6 +37141,7 @@ var runGitConvertCommand = async ({
36741
37141
  storeRef
36742
37142
  });
36743
37143
  const result = await pushCheckout({
37144
+ ignoreSettings: "preview",
36744
37145
  client,
36745
37146
  force,
36746
37147
  ignorePatterns: conversionIgnorePatterns,
@@ -36761,8 +37162,8 @@ var runGitConvertCommand = async ({
36761
37162
  "Aborted before deleting Git metadata. The Forum push completed, but .git and .gitignore were left in place."
36762
37163
  );
36763
37164
  }
36764
- await rm4(path19.join(localPath, ".gitignore"), { force: true });
36765
- 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 });
36766
37167
  renderOutput(
36767
37168
  `${created ? "Created" : "Using existing"} store ${metadata.slug}`
36768
37169
  );
@@ -36880,7 +37281,7 @@ var main = async (argv = process.argv.slice(2)) => {
36880
37281
  }
36881
37282
  if (command === "hash" && parsed.local) {
36882
37283
  const filePath = rest[0];
36883
- renderOutput(await hashLocalFile(path19.resolve(filePath)));
37284
+ renderOutput(await hashLocalFile(path21.resolve(filePath)));
36884
37285
  return;
36885
37286
  }
36886
37287
  const config2 = await resolveConfig(parsed);
@@ -37154,26 +37555,45 @@ var main = async (argv = process.argv.slice(2)) => {
37154
37555
  const subcommand = rest[0];
37155
37556
  const checkout = await resolveCheckoutConfig();
37156
37557
  assertCheckoutSyncEnabled(checkout);
37157
- const storeRef = await resolveProjectStoreRef();
37158
- 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)
37159
37564
  throw new Error(
37160
- "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."
37161
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 ?? [];
37162
37586
  }
37163
- const metadata = await client.readStoreByRef(storeRef);
37164
- const currentPatterns = metadata?.ignore ?? [];
37165
37587
  await runIgnoreCommand({
37166
- checkout,
37167
- client,
37588
+ writePatterns: (patterns) => writeSharedIgnoreFile(sharedFilePath, patterns),
37168
37589
  commandNames,
37169
37590
  confirmYesNo,
37170
- currentPatterns,
37591
+ currentPatterns: currentPatterns ?? [],
37171
37592
  analyzeLocal: () => analyzeLocalIgnore(checkout),
37172
37593
  markOnboarded: () => markIgnoreOnboarded(checkout.rootDir),
37173
37594
  pattern: rest[1],
37174
37595
  patterns: rest.slice(1),
37175
37596
  renderOutput,
37176
- storeRef,
37177
37597
  subcommand,
37178
37598
  assumeYes: parsed.yes
37179
37599
  });
@@ -37252,7 +37672,7 @@ var main = async (argv = process.argv.slice(2)) => {
37252
37672
  }
37253
37673
  const checkout = await resolveCheckoutConfig();
37254
37674
  assertCheckoutSyncEnabled(checkout);
37255
- await runIgnoreSetupGate(checkout, client, parsed.yes);
37675
+ if (!parsed.dryRun) await runIgnoreSetupGate(checkout, client, parsed.yes);
37256
37676
  await runCheckoutPush({
37257
37677
  checkout,
37258
37678
  client,
@@ -37265,12 +37685,41 @@ var main = async (argv = process.argv.slice(2)) => {
37265
37685
  return;
37266
37686
  }
37267
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
+ }
37268
37693
  if (rest[0] === "reset") {
37269
37694
  const checkout = await resolveCheckoutConfig();
37270
37695
  await resetCheckoutSyncState(checkout.rootDir);
37271
37696
  renderOutput("Cleared local checkout sync state.");
37272
37697
  return;
37273
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
+ }
37274
37723
  if (parsed.dryRun) {
37275
37724
  await withActivity(
37276
37725
  "Checking status",
@@ -37284,6 +37733,7 @@ var main = async (argv = process.argv.slice(2)) => {
37284
37733
  }
37285
37734
  if (parsed.watch) {
37286
37735
  await runCheckoutSyncWatch({
37736
+ conflictPolicy: parsed.conflictPolicy,
37287
37737
  client,
37288
37738
  force: parsed.force,
37289
37739
  intervalMs: config2.pollIntervalMs,
@@ -37311,6 +37761,7 @@ var main = async (argv = process.argv.slice(2)) => {
37311
37761
  }
37312
37762
  await runIgnoreSetupGate(await resolveCheckoutConfig(), client, parsed.yes);
37313
37763
  await runCheckoutSyncOnce({
37764
+ conflictPolicy: parsed.conflictPolicy,
37314
37765
  client,
37315
37766
  force: parsed.force,
37316
37767
  verbose: parsed.verbose,
@@ -37352,8 +37803,8 @@ var main = async (argv = process.argv.slice(2)) => {
37352
37803
  `Usage: ${commandNames} clone <storeId> [localPath] [--readonly]`
37353
37804
  );
37354
37805
  }
37355
- const localPath = path19.resolve(
37356
- rest[1] ?? path19.join(process.cwd(), defaultCloneDirectoryName(remoteInput))
37806
+ const localPath = path21.resolve(
37807
+ rest[1] ?? path21.join(process.cwd(), defaultCloneDirectoryName(remoteInput))
37357
37808
  );
37358
37809
  const { metadata, workspaceSlug, storeRef } = await resolveReadableStore(
37359
37810
  remoteInput,
@@ -37371,14 +37822,17 @@ var main = async (argv = process.argv.slice(2)) => {
37371
37822
  verbose: parsed.verbose
37372
37823
  });
37373
37824
  const result = await pullCheckout({
37825
+ ignoreSettings: "preview",
37374
37826
  client,
37375
37827
  ignorePatterns: checkout.ignore,
37828
+ ignorePatternsByStore: checkout.ignorePatternsByStore,
37829
+ ignoreMatchersByRule: checkout.ignoreMatchersByRule,
37376
37830
  localPath: checkout.rootDir,
37377
37831
  progress,
37378
37832
  storeRef,
37379
37833
  syncRules: checkout.syncRules
37380
37834
  });
37381
- renderOutput(`Cloned store ${metadata.slug} to ${path19.resolve(localPath)}`);
37835
+ renderOutput(`Cloned store ${metadata.slug} to ${path21.resolve(localPath)}`);
37382
37836
  formatCheckoutResult("Pulled", result, {
37383
37837
  detailsRendered: Boolean(progress),
37384
37838
  verbose: parsed.verbose