@factorialco/fcode-cli 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -71,16 +71,20 @@ function readLocale(workspaceDir, locale) {
71
71
  ...readLocaleFile(workspaceDir, locale),
72
72
  };
73
73
  }
74
- function chosenPrimaryLocaleOf(workspaceDir) {
74
+ function primaryLocaleIn(workspaceDir, fileName) {
75
75
  try {
76
76
  (0, settings_1.migrateLegacySettingsFile)(workspaceDir);
77
- const settings = fs.readJsonSync(path.join(workspaceDir, settings_1.SETTINGS_FILENAME));
77
+ const settings = fs.readJsonSync(path.join(workspaceDir, fileName));
78
78
  return typeof settings?.primaryLocale === 'string' && settings.primaryLocale.length > 0 ? settings.primaryLocale : undefined;
79
79
  }
80
80
  catch {
81
81
  return undefined;
82
82
  }
83
83
  }
84
+ /** The workspace's own choice, or the one it inherits when settings.json makes none. */
85
+ function chosenPrimaryLocaleOf(workspaceDir) {
86
+ return primaryLocaleIn(workspaceDir, settings_1.SETTINGS_FILENAME) ?? primaryLocaleIn(workspaceDir, settings_1.INHERITED_SETTINGS_FILENAME);
87
+ }
84
88
  /** Every locale file in the workspace, own and inherited: `pull` writes them all to `i18n/`. */
85
89
  function localeFileSlugs(workspaceDir) {
86
90
  const directory = path.join(workspaceDir, I18N_DIRECTORY);
@@ -120,6 +124,20 @@ function matchingLocale(candidate, available) {
120
124
  }
121
125
  return available.filter((locale) => locale.toLowerCase() === candidate.toLowerCase()).sort()[0];
122
126
  }
127
+ const LOCALE_SEPARATOR = /[-_]/;
128
+ /**
129
+ * The available locale a bare language broadens to when the workspace only has regional files for
130
+ * it (`es` → `es-ES`): one whose first segment is the language, case ignored — the primary locale
131
+ * if it is one of them, the alphabetically first otherwise. A language with a separator in it (an
132
+ * identifier that starts with one) broadens to nothing. Mirrors the platform's `LocaleFallbackChain`.
133
+ */
134
+ function sameLanguageLocale(language, primary, available) {
135
+ if (!language || LOCALE_SEPARATOR.test(language)) {
136
+ return undefined;
137
+ }
138
+ const sameLanguage = available.filter((locale) => locale.split(LOCALE_SEPARATOR)[0].toLowerCase() === language.toLowerCase()).sort();
139
+ return primary !== undefined && sameLanguage.includes(primary) ? primary : sameLanguage[0];
140
+ }
123
141
  /**
124
142
  * The locale files that answer a requested identifier, lowest precedence first. Mirrors the
125
143
  * platform's `LocaleFallbackChain`, so a local run and the cloud resolve the same text.
@@ -128,7 +146,9 @@ function matchingLocale(candidate, available) {
128
146
  * publishes plain languages, so the request degrades one segment at a time — split on `-` or `_`,
129
147
  * without parsing a language tag, since any short slug is a legal identifier here — and every step
130
148
  * that matches stays in the chain: a variant file translates the keys that differ and leaves the
131
- * rest to the language it came from.
149
+ * rest to the language it came from. The opposite happens too — the host sends `es` while the
150
+ * workspace has `es-ES` — so a language layer that matches nothing, by name or ignoring case, is
151
+ * broadened to a regional file of that language, in the place the language would have taken.
132
152
  */
133
153
  function localeFallbackChain(requested, primary, available) {
134
154
  const chain = [];
@@ -146,8 +166,14 @@ function localeFallbackChain(requested, primary, available) {
146
166
  const lastSeparator = Math.max(candidate.lastIndexOf('-'), candidate.lastIndexOf('_'));
147
167
  candidate = lastSeparator > 0 ? candidate.slice(0, lastSeparator) : undefined;
148
168
  }
149
- for (const shorterOrExact of candidates)
150
- add(shorterOrExact);
169
+ candidates.forEach((shorterOrExact, index) => {
170
+ if (index === 0 && !matchingLocale(shorterOrExact, available)) {
171
+ add(sameLanguageLocale(shorterOrExact, primary, available));
172
+ }
173
+ else {
174
+ add(shorterOrExact);
175
+ }
176
+ });
151
177
  return chain;
152
178
  }
153
179
  /**
@@ -156,14 +182,15 @@ function localeFallbackChain(requested, primary, available) {
156
182
  * shorter form of the requested one the workspace publishes — layered underneath, plus each
157
183
  * locale's own map (inherited file layered under its own, but NOT merged over the primary) so a
158
184
  * call naming a locale (`fcode.i18n(key, args, { locale })`) resolves like the cloud's `by_locale`:
159
- * key by key in the requested locale first, falling back to the merged translations
185
+ * key by key in the requested locale first, falling back to the merged translations — and the
186
+ * primary locale, which a call naming a bare language prefers when it broadens to a regional file
160
187
  */
161
188
  function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd()) {
162
189
  const primary = primaryLocaleOf(workspaceDir);
163
190
  const locale = requestedLocale || primary;
164
191
  const byLocale = Object.fromEntries(availableLocales(workspaceDir).map((slug) => [slug, readLocale(workspaceDir, slug)]));
165
192
  if (!locale) {
166
- return { locale: undefined, translations: {}, byLocale };
193
+ return { locale: undefined, primaryLocale: primary, translations: {}, byLocale };
167
194
  }
168
195
  // `byLocale` already holds every locale's own file, so the merged map — the fallback chain
169
196
  // layered lowest precedence first — is derived from it rather than read from disk a second time.
@@ -171,5 +198,5 @@ function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd())
171
198
  for (const layer of localeFallbackChain(requestedLocale, primary, Object.keys(byLocale))) {
172
199
  Object.assign(translations, byLocale[layer]);
173
200
  }
174
- return { locale, translations, byLocale };
201
+ return { locale, primaryLocale: primary, translations, byLocale };
175
202
  }
@@ -13,9 +13,11 @@ export type WebhookAuthResult = {
13
13
  */
14
14
  export declare function findHeader(headers: http.IncomingHttpHeaders, name: string): string | undefined;
15
15
  /**
16
- * The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM.
16
+ * The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM. The
17
+ * workspace's is its own (settings.json) or, when settings.json leaves it out, the inherited one
18
+ * (settings.inherited.json); null in settings.json clears it, a parent's included.
17
19
  * Returns null only when the webhook is public — TEAM without a workspace configuration throws,
18
20
  * because treating it as public would silently unprotect a webhook its owner chose to protect.
19
21
  */
20
- export declare function resolveEffectiveAuth(webhook: ProcessWebhook | undefined, teamWebhookAuth: WebhookAuth | undefined): WebhookAuth | null;
22
+ export declare function resolveEffectiveAuth(webhook: ProcessWebhook | undefined, teamWebhookAuth: WebhookAuth | null | undefined, inheritedTeamWebhookAuth?: WebhookAuth): WebhookAuth | null;
21
23
  export declare function checkWebhookAuth(effectiveAuth: WebhookAuth | null, headers: http.IncomingHttpHeaders, variableValue: string | undefined): WebhookAuthResult;
@@ -26,20 +26,26 @@ function findHeader(headers, name) {
26
26
  return undefined;
27
27
  }
28
28
  /**
29
- * The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM.
29
+ * The configuration a webhook actually enforces: its own when CUSTOM, the workspace's when TEAM. The
30
+ * workspace's is its own (settings.json) or, when settings.json leaves it out, the inherited one
31
+ * (settings.inherited.json); null in settings.json clears it, a parent's included.
30
32
  * Returns null only when the webhook is public — TEAM without a workspace configuration throws,
31
33
  * because treating it as public would silently unprotect a webhook its owner chose to protect.
32
34
  */
33
- function resolveEffectiveAuth(webhook, teamWebhookAuth) {
35
+ function resolveEffectiveAuth(webhook, teamWebhookAuth, inheritedTeamWebhookAuth) {
34
36
  const authMode = webhook?.authMode;
35
37
  if (!authMode || authMode === 'NONE') {
36
38
  return null;
37
39
  }
38
40
  if (authMode === 'TEAM') {
39
- if (!teamWebhookAuth) {
40
- throw new Error('this webhook inherits the workspace webhook auth, which is not set in settings.json');
41
+ if (teamWebhookAuth === null) {
42
+ throw new Error('this webhook inherits the workspace webhook auth, which settings.json sets to none');
41
43
  }
42
- return teamWebhookAuth;
44
+ const workspaceAuth = teamWebhookAuth ?? inheritedTeamWebhookAuth;
45
+ if (!workspaceAuth) {
46
+ throw new Error('this webhook inherits the workspace webhook auth, which is not set in settings.json nor inherited');
47
+ }
48
+ return workspaceAuth;
43
49
  }
44
50
  if (!webhook?.auth?.variableKey) {
45
51
  throw new Error('this webhook uses the CUSTOM auth mode but names no variable in metadata.json');
@@ -0,0 +1,7 @@
1
+ /** The header the CLI reports its version in, so the platform can refuse a CLI too old for a request. */
2
+ export declare const CLI_VERSION_HEADER = "Fcode-Cli-Version";
3
+ /**
4
+ * The installed CLI's own version, read from its package.json: two levels above this file both in
5
+ * src/utils (tests) and dist/utils (the published package, which always ships package.json).
6
+ */
7
+ export declare function cliVersion(): string;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLI_VERSION_HEADER = void 0;
4
+ exports.cliVersion = cliVersion;
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ /** The header the CLI reports its version in, so the platform can refuse a CLI too old for a request. */
8
+ exports.CLI_VERSION_HEADER = 'Fcode-Cli-Version';
9
+ let cached;
10
+ /**
11
+ * The installed CLI's own version, read from its package.json: two levels above this file both in
12
+ * src/utils (tests) and dist/utils (the published package, which always ships package.json).
13
+ */
14
+ function cliVersion() {
15
+ if (cached === undefined) {
16
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8'));
17
+ cached = String(packageJson.version);
18
+ }
19
+ return cached;
20
+ }
@@ -16,6 +16,7 @@ export declare function renderGitignoreWithInherited(existing: string, inherited
16
16
  * current workspace metadata, so resources that stop being inherited drop out.
17
17
  * Inherited variable values live in one file, which is ignored as soon as the
18
18
  * workspace inherits any variable; inherited locales are one file each, like modules;
19
- * inherited dependencies are one file per language, like variables.
19
+ * inherited dependencies are one file per language, like variables; inherited settings are one
20
+ * file.
20
21
  */
21
22
  export declare function syncInheritedGitignore(workspaceDir?: string): Promise<void>;
@@ -9,6 +9,7 @@ const workspace_1 = require("../config/workspace");
9
9
  const global_1 = require("../config/global");
10
10
  const settings_1 = require("../config/settings");
11
11
  const programming_language_manifest_1 = require("../repositories/programming-language-manifest");
12
+ const settings_2 = require("../repositories/settings");
12
13
  const filenamify_1 = require("./filenamify");
13
14
  const file_utils_1 = require("./file-utils");
14
15
  exports.INHERITED_BLOCK_START = '# BEGIN fcode inherited resources (auto-generated, do not edit)';
@@ -51,7 +52,8 @@ function stripManagedBlock(content) {
51
52
  * current workspace metadata, so resources that stop being inherited drop out.
52
53
  * Inherited variable values live in one file, which is ignored as soon as the
53
54
  * workspace inherits any variable; inherited locales are one file each, like modules;
54
- * inherited dependencies are one file per language, like variables.
55
+ * inherited dependencies are one file per language, like variables; inherited settings are one
56
+ * file.
55
57
  */
56
58
  async function syncInheritedGitignore(workspaceDir = process.cwd()) {
57
59
  const team = await new workspace_1.default(workspaceDir).getActiveTeam();
@@ -71,6 +73,8 @@ async function syncInheritedGitignore(workspaceDir = process.cwd()) {
71
73
  // inherit — checked on disk because the manifest metadata tracks the workspace's own list, not
72
74
  // what its parents contribute.
73
75
  ...exports.INHERITED_DEPENDENCY_FILES.filter((file) => fs.existsSync(path.join(workspaceDir, file))),
76
+ // Written only when the workspace inherits a setting, so checked on disk like dependencies.
77
+ ...(fs.existsSync(path.join(workspaceDir, settings_2.INHERITED_SETTINGS_FILENAME)) ? [settings_2.INHERITED_SETTINGS_FILENAME] : []),
74
78
  ];
75
79
  const gitignorePath = path.join(workspaceDir, '.gitignore');
76
80
  const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
@@ -2516,5 +2516,5 @@
2516
2516
  ]
2517
2517
  }
2518
2518
  },
2519
- "version": "3.5.0"
2519
+ "version": "3.6.0"
2520
2520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorialco/fcode-cli",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "packageManager": "pnpm@11.7.0",
5
5
  "description": "Factorial Code Command Line Interface",
6
6
  "homepage": "https://code.factorialhr.com/docs/cli/",