@fenglimg/fabric-shared 2.1.0-rc.2 → 2.2.0-rc.10

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.
@@ -0,0 +1,251 @@
1
+ // src/i18n/normalize-locale.ts
2
+ function normalizeLocale(raw) {
3
+ if (typeof raw !== "string") {
4
+ return "en";
5
+ }
6
+ const normalized = raw.trim().toLowerCase().replace(/\..*$/, "").replace(/_/g, "-");
7
+ if (normalized.length === 0) {
8
+ return "en";
9
+ }
10
+ if (normalized === "zh" || normalized.startsWith("zh-")) {
11
+ return "zh-CN";
12
+ }
13
+ return "en";
14
+ }
15
+
16
+ // src/i18n/detect-node-locale.ts
17
+ function detectNodeLocale() {
18
+ const fromFabricEnv = process.env.FAB_LANG;
19
+ if (typeof fromFabricEnv === "string" && fromFabricEnv.trim().length > 0) {
20
+ return normalizeLocale(fromFabricEnv);
21
+ }
22
+ const fromLangEnv = process.env.LANG;
23
+ if (typeof fromLangEnv === "string" && fromLangEnv.trim().length > 0) {
24
+ return normalizeLocale(fromLangEnv);
25
+ }
26
+ return "en";
27
+ }
28
+
29
+ // src/schemas/store.ts
30
+ import { z } from "zod";
31
+ var STORE_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
32
+ var storeUuidSchema = z.string().regex(STORE_UUID_PATTERN, "store_uuid must be a canonical lowercase UUID");
33
+ var STORE_ALIAS_PATTERN = /^(?!\.{1,2}$)[A-Za-z0-9._-]{1,80}$/u;
34
+ var storeAliasSchema = z.string().regex(
35
+ STORE_ALIAS_PATTERN,
36
+ "store alias must be a single [A-Za-z0-9._-] path segment, max 80 chars"
37
+ );
38
+ var PERSONAL_STORE_SENTINEL = "$personal";
39
+ var storeIdentitySchema = z.object({
40
+ // Intrinsic, immutable once minted. Read from store.json, never recomputed.
41
+ store_uuid: storeUuidSchema,
42
+ // ISO-8601. When the store was first initialized.
43
+ created_at: z.string(),
44
+ // Optional human-facing canonical alias baked into the store (e.g. the
45
+ // team picks "platform-kb"). Local per-machine aliases are resolved by the
46
+ // StoreResolver from config and may differ; this is the suggested default.
47
+ canonical_alias: storeAliasSchema.optional(),
48
+ // Optional one-line description surfaced in `store list` / onboarding.
49
+ description: z.string().optional(),
50
+ // The semantic scopes this store is *allowed* to hold. A shared (team)
51
+ // store MUST NOT list "personal" (R5#3 privacy boundary, enforced at write
52
+ // time in P2). Open coordinate strings — see schemas/scope.ts.
53
+ allowed_scopes: z.array(z.string()).optional()
54
+ }).strict();
55
+ var STORE_PROJECT_ID_PATTERN = /^[a-z0-9_-]+$/u;
56
+ var STORE_MOUNT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,78}[a-z0-9]$/u;
57
+ var storeMountNameSchema = z.string().regex(
58
+ STORE_MOUNT_NAME_PATTERN,
59
+ "mount_name must be lowercase [a-z0-9._-], start/end with alnum, max 80 chars"
60
+ ).refine((value) => value !== "." && value !== "..", "mount_name cannot be . or ..");
61
+ var storeProjectSchema = z.object({
62
+ // Single scope segment forming the `project:<id>` coordinate. Immutable.
63
+ id: z.string().regex(STORE_PROJECT_ID_PATTERN, "project id must be a single lowercase [a-z0-9_-] segment"),
64
+ // Optional human-facing label surfaced in `store project list`.
65
+ name: z.string().optional(),
66
+ // ISO-8601. When the project was first registered in this store.
67
+ created_at: z.string()
68
+ }).strict();
69
+ var storeProjectsFileSchema = z.object({
70
+ projects: z.array(storeProjectSchema).default([])
71
+ }).strict();
72
+ var requiredStoreEntrySchema = z.object({
73
+ id: z.string().min(1),
74
+ suggested_remote: z.union([z.string().min(1), z.literal(PERSONAL_STORE_SENTINEL)]).optional()
75
+ }).strict();
76
+ var STORE_KNOWLEDGE_TYPE_DIRS = [
77
+ "models",
78
+ "decisions",
79
+ "guidelines",
80
+ "pitfalls",
81
+ "processes"
82
+ ];
83
+ var STORE_LAYOUT = {
84
+ identityFile: "store.json",
85
+ // Store-internal project registry (W1/A2). Committed parallel to store.json.
86
+ projectsFile: "projects.json",
87
+ // v2.2 W4 (agents.meta decolo) — per-store monotonic stable_id counters.
88
+ // COMMITTED parallel to store.json/projects.json (NOT gitignored like the
89
+ // derived agents.meta) because the counter ledger is non-derivable state that
90
+ // must travel with the store on clone: a fresh clone rebuilding from disk-max
91
+ // would re-mint a deleted entry's id and corrupt cite history (KT-DEC-0004
92
+ // monotonic invariant). Replaces the retired co-location
93
+ // <projectRoot>/.fabric/agents.meta.json#counters.
94
+ countersFile: "counters.json",
95
+ knowledgeDir: "knowledge",
96
+ bindingsDir: "bindings",
97
+ stateDir: "state"
98
+ };
99
+ var STORES_ROOT_DIR = "stores";
100
+ var GLOBAL_STATE_DIR = "state";
101
+ var GLOBAL_BINDINGS_DIR = "bindings";
102
+ function storeKnowledgeTypeDir(type) {
103
+ return `${STORE_LAYOUT.knowledgeDir}/${type}`;
104
+ }
105
+ function storeRelativePath(storeUuid) {
106
+ return `${STORES_ROOT_DIR}/${storeUuid}`;
107
+ }
108
+ var STORE_MOUNT_GROUPS = ["personal", "team"];
109
+ function storeMountGroup(store) {
110
+ return store.personal === true ? "personal" : "team";
111
+ }
112
+ function storeMountSubPath(store) {
113
+ return `${storeMountGroup(store)}/${store.mount_name ?? store.store_uuid}`;
114
+ }
115
+ function storeRelativePathForMount(store) {
116
+ return `${STORES_ROOT_DIR}/${storeMountSubPath(store)}`;
117
+ }
118
+ function sanitizeMountLabel(raw) {
119
+ const slug = raw.toLowerCase().replace(/[^a-z0-9._-]+/gu, "-").replace(/[-._]{2,}/gu, "-").replace(/^[^a-z0-9]+/u, "").replace(/[^a-z0-9]+$/u, "").slice(0, 80).replace(/[^a-z0-9]+$/u, "");
120
+ return STORE_MOUNT_NAME_PATTERN.test(slug) ? slug : void 0;
121
+ }
122
+ function mountLabelFromRemote(remote) {
123
+ const withoutGit = remote.trim().replace(/\.git$/iu, "").replace(/\/+$/u, "");
124
+ const lastSegment = withoutGit.split(/[\\/:]/u).filter(Boolean).at(-1);
125
+ return lastSegment === void 0 ? void 0 : sanitizeMountLabel(lastSegment);
126
+ }
127
+ function deriveMountLabel(input) {
128
+ const fromRemote = input.remote === void 0 ? void 0 : mountLabelFromRemote(input.remote);
129
+ if (fromRemote !== void 0) {
130
+ return fromRemote;
131
+ }
132
+ if (input.alias !== void 0) {
133
+ const fromAlias = sanitizeMountLabel(input.alias);
134
+ if (fromAlias !== void 0) {
135
+ return fromAlias;
136
+ }
137
+ }
138
+ return input.store_uuid.replace(/-/gu, "").slice(0, 8);
139
+ }
140
+ var mountedStoreSchema = z.object({
141
+ // Intrinsic identity of the mounted store (matches its store.json).
142
+ store_uuid: storeUuidSchema,
143
+ // Stable human-readable local directory under ~/.fabric/stores/. When absent,
144
+ // older uuid-named mounts stay valid and resolve to stores/<store_uuid>.
145
+ mount_name: storeMountNameSchema.optional(),
146
+ // Local per-machine alias the user references this store by (resolver maps
147
+ // alias → uuid). May differ from the store's canonical_alias.
148
+ alias: storeAliasSchema,
149
+ // Optional user-facing label. Does not participate in resolution.
150
+ display_name: z.string().optional(),
151
+ // Git remote locator for this clone, if any. Absent = local-only store
152
+ // (valid; doctor nudges to add a remote for backup — R5#5, P6).
153
+ remote: z.string().min(1).optional(),
154
+ // v2.1.0-rc.1 P3: marks the implicit personal store (the one minted by
155
+ // `install --global`). Exactly one mounted store carries personal=true; it
156
+ // is the write target for personal-scope entries (R5#3) and always in the
157
+ // read-set (S11). Optional (no default) so the output type stays a plain
158
+ // optional — consumers coalesce `?? false` when building resolver input.
159
+ personal: z.boolean().optional(),
160
+ // Whether writes are accepted into this store from this machine. Optional;
161
+ // consumers coalesce `?? true`. Shared stores cloned read-only set false.
162
+ writable: z.boolean().optional()
163
+ }).strict();
164
+ var globalConfigSchema = z.object({
165
+ // Machine/account identity. Personal-knowledge id namespace (S33/S27).
166
+ uid: z.string().min(1),
167
+ // grill-6fixes (D1): the single machine-wide language base tone. Governs
168
+ // BOTH the CLI display locale AND the knowledge-authoring language — there
169
+ // is no per-project override (the old project `fabric_language` +
170
+ // README-detection path was removed). Picked once via the install
171
+ // language selector; changeable via `fabric config`. Absent ⇒ resolvers
172
+ // fall back to env detection (FAB_LANG → LANG → en).
173
+ language: z.enum(["zh-CN", "en"]).optional(),
174
+ // All stores mounted on this machine. The implicit personal store is
175
+ // included here once initialized. Default empty so a fresh global config
176
+ // (before `install --global`) parses cleanly.
177
+ stores: z.array(mountedStoreSchema).optional().default([])
178
+ }).passthrough();
179
+
180
+ // src/store/global-config-io.ts
181
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
182
+ import { homedir } from "os";
183
+ import { join } from "path";
184
+ function resolveGlobalRoot() {
185
+ return join(process.env.FABRIC_HOME ?? homedir(), ".fabric");
186
+ }
187
+ function globalConfigPath(globalRoot = resolveGlobalRoot()) {
188
+ return join(globalRoot, "fabric-global.json");
189
+ }
190
+ function loadGlobalConfig(globalRoot = resolveGlobalRoot()) {
191
+ const path = globalConfigPath(globalRoot);
192
+ if (!existsSync(path)) {
193
+ return null;
194
+ }
195
+ return globalConfigSchema.parse(JSON.parse(readFileSync(path, "utf8")));
196
+ }
197
+ function saveGlobalConfig(config, globalRoot = resolveGlobalRoot()) {
198
+ const validated = globalConfigSchema.parse(config);
199
+ mkdirSync(globalRoot, { recursive: true });
200
+ writeFileSync(globalConfigPath(globalRoot), `${JSON.stringify(validated, null, 2)}
201
+ `, "utf8");
202
+ }
203
+
204
+ // src/i18n/resolve-global-locale.ts
205
+ function resolveGlobalLocale(globalRoot) {
206
+ try {
207
+ const config = globalRoot === void 0 ? loadGlobalConfig() : loadGlobalConfig(globalRoot);
208
+ const language = config?.language;
209
+ if (language === "zh-CN" || language === "en") {
210
+ return language;
211
+ }
212
+ } catch {
213
+ }
214
+ return detectNodeLocale();
215
+ }
216
+
217
+ export {
218
+ normalizeLocale,
219
+ detectNodeLocale,
220
+ STORE_UUID_PATTERN,
221
+ storeUuidSchema,
222
+ STORE_ALIAS_PATTERN,
223
+ storeAliasSchema,
224
+ PERSONAL_STORE_SENTINEL,
225
+ storeIdentitySchema,
226
+ STORE_PROJECT_ID_PATTERN,
227
+ STORE_MOUNT_NAME_PATTERN,
228
+ storeMountNameSchema,
229
+ storeProjectSchema,
230
+ storeProjectsFileSchema,
231
+ requiredStoreEntrySchema,
232
+ STORE_KNOWLEDGE_TYPE_DIRS,
233
+ STORE_LAYOUT,
234
+ STORES_ROOT_DIR,
235
+ GLOBAL_STATE_DIR,
236
+ GLOBAL_BINDINGS_DIR,
237
+ storeKnowledgeTypeDir,
238
+ storeRelativePath,
239
+ STORE_MOUNT_GROUPS,
240
+ storeMountGroup,
241
+ storeMountSubPath,
242
+ storeRelativePathForMount,
243
+ deriveMountLabel,
244
+ mountedStoreSchema,
245
+ globalConfigSchema,
246
+ resolveGlobalRoot,
247
+ globalConfigPath,
248
+ loadGlobalConfig,
249
+ saveGlobalConfig,
250
+ resolveGlobalLocale
251
+ };