@factorialco/fcode-cli 3.5.0 → 3.5.1

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/README.md CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @factorialco/fcode-cli
20
20
  $ fcode COMMAND
21
21
  running command...
22
22
  $ fcode (--version)
23
- @factorialco/fcode-cli/3.5.0 linux-x64 node-v22.22.3
23
+ @factorialco/fcode-cli/3.5.1 linux-x64 node-v22.22.3
24
24
  $ fcode --help [COMMAND]
25
25
  USAGE
26
26
  $ fcode COMMAND
@@ -606,25 +606,38 @@ const main = typeof _hooksModule['${fn}'] === 'function'
606
606
  i18n: Object.assign((key, args, options) => {
607
607
  const translations = ${JSON.stringify(metadata.i18n.translations, null, 2)};
608
608
  const byLocale = ${JSON.stringify(metadata.i18n.byLocale, null, 2)};
609
+ const primaryLocale = ${JSON.stringify(metadata.i18n.primaryLocale ?? null)};
609
610
  const requested = options && typeof options === 'object' && options.locale !== undefined && options.locale !== null
610
611
  ? String(options.locale)
611
612
  : undefined;
612
613
  // An identifier no locale matches is shortened one segment at a time ('en-gb' reads
613
- // 'en'), by name first and ignoring case only as a last resort — the same chain the
614
- // cloud resolves, so a local run answers a regional variant the same way.
614
+ // 'en'), by name first and ignoring case only as a last resort, and a language that
615
+ // still matches nothing is broadened to a regional locale of it ('es' reads 'es-ES',
616
+ // the primary one if it is among them, else the alphabetically first) — the same chain
617
+ // the cloud resolves, so a local run answers the same identifiers the same way.
618
+ const names = Object.keys(byLocale);
619
+ const matching = (candidate) => names.includes(candidate)
620
+ ? candidate
621
+ : names.filter((name) => name.toLowerCase() === candidate.toLowerCase()).sort()[0];
615
622
  const localeMaps = [];
616
- for (let candidate = requested; candidate; ) {
617
- const names = Object.keys(byLocale);
618
- const lowered = candidate.toLowerCase();
619
- const match = names.includes(candidate)
620
- ? candidate
621
- : names.filter((name) => name.toLowerCase() === lowered).sort()[0];
623
+ const add = (match) => {
622
624
  if (match !== undefined && !localeMaps.includes(byLocale[match])) {
623
625
  localeMaps.push(byLocale[match]);
624
626
  }
627
+ };
628
+ let language;
629
+ for (let candidate = requested; candidate; ) {
630
+ add(matching(candidate));
631
+ language = candidate;
625
632
  const lastSeparator = Math.max(candidate.lastIndexOf('-'), candidate.lastIndexOf('_'));
626
633
  candidate = lastSeparator > 0 ? candidate.slice(0, lastSeparator) : undefined;
627
634
  }
635
+ if (language !== undefined && !/[-_]/.test(language) && matching(language) === undefined) {
636
+ const sameLanguage = names
637
+ .filter((name) => name.split(/[-_]/)[0].toLowerCase() === language.toLowerCase())
638
+ .sort();
639
+ add(sameLanguage.includes(primaryLocale) ? primaryLocale : sameLanguage[0]);
640
+ }
628
641
  let template;
629
642
  for (const localeMap of localeMaps) {
630
643
  template = Object.hasOwn(localeMap, key) ? localeMap[key] : undefined;
@@ -96,6 +96,7 @@ class I18n:
96
96
  payload = i18n_payload or {}
97
97
  self.__translations = payload.get("translations") or {}
98
98
  self.__by_locale = payload.get("byLocale") or {}
99
+ self.__primary_locale = payload.get("primaryLocale")
99
100
  self.locale = payload.get("locale")
100
101
 
101
102
  def __call__(self, key, args=None, options=None):
@@ -125,24 +126,43 @@ class I18n:
125
126
  """The maps a requested locale opens, most specific first.
126
127
 
127
128
  An identifier no locale matches is shortened one segment at a time ("en-gb" reads "en"),
128
- by name first and ignoring case only as a last resort -- the same chain the cloud
129
- executors resolve, so a local run answers a regional variant the same way.
129
+ by name first and ignoring case only as a last resort, and a language that still matches
130
+ nothing is broadened to a regional locale of it ("es" reads "es-ES", the primary one if it
131
+ is among them, else the alphabetically first) -- the same chain the cloud executors
132
+ resolve, so a local run answers the same identifiers the same way.
130
133
  """
131
134
  if locale is None or not self.__by_locale:
132
135
  return []
136
+ import re
137
+
133
138
  names = list(self.__by_locale.keys())
134
139
  chain = []
135
- candidate = locale
136
- while candidate:
140
+
141
+ def matching(candidate):
137
142
  if candidate in names:
138
- match = candidate
139
- else:
140
- folded = sorted(n for n in names if n.lower() == candidate.lower())
141
- match = folded[0] if folded else None
143
+ return candidate
144
+ folded = sorted(n for n in names if n.lower() == candidate.lower())
145
+ return folded[0] if folded else None
146
+
147
+ def add(match):
142
148
  if match is not None and match not in chain:
143
149
  chain.append(match)
150
+
151
+ candidate = locale
152
+ language = None
153
+ while candidate:
154
+ add(matching(candidate))
155
+ language = candidate
144
156
  last_separator = max(candidate.rfind("-"), candidate.rfind("_"))
145
157
  candidate = candidate[:last_separator] if last_separator > 0 else None
158
+ if language and not re.search(r"[-_]", language) and matching(language) is None:
159
+ same_language = sorted(
160
+ n for n in names if re.split(r"[-_]", n)[0].lower() == language.lower()
161
+ )
162
+ if self.__primary_locale in same_language:
163
+ add(self.__primary_locale)
164
+ elif same_language:
165
+ add(same_language[0])
146
166
  return [self.__by_locale[name] for name in chain if self.__by_locale[name]]
147
167
 
148
168
 
@@ -4,6 +4,11 @@ const graphql_request_1 = require("graphql-request");
4
4
  const types_1 = require("../core/types");
5
5
  const locale_merger_1 = require("./locale-merger");
6
6
  const _ = require("lodash");
7
+ // Source code travels base64 encoded: Cloudflare's WAF blocks requests whose body
8
+ // looks like an attack payload, which source code often does. sourceCodeEncoding tells the
9
+ // API to decode it back.
10
+ const SOURCE_CODE_ENCODING = 'BASE64';
11
+ const encodeSourceCode = (sourceCode) => sourceCode == null ? sourceCode : Buffer.from(sourceCode, 'utf8').toString('base64');
7
12
  // Manifest fields an fcode-api older than this CLI may not know. The manifest query falls back to a
8
13
  // narrower selection when the error names one of them, so a CLI stays usable against an
9
14
  // on-premises api that has not been updated yet.
@@ -261,8 +266,19 @@ class FcodeGraphQLClient {
261
266
  createVersionedProcesses(versionedProcess) {
262
267
  return this.client
263
268
  .request((0, graphql_request_1.gql) `
264
- mutation publishProcess($processId: ID!, $tag: String!, $sourceCode: String, $parametersSchema: JSON, $readme: String) {
265
- publishProcess(processId: $processId, tag: $tag, input: { sourceCode: $sourceCode, parametersSchema: $parametersSchema, readme: $readme }) {
269
+ mutation publishProcess(
270
+ $processId: ID!
271
+ $tag: String!
272
+ $sourceCode: String
273
+ $sourceCodeEncoding: SourceCodeEncoding
274
+ $parametersSchema: JSON
275
+ $readme: String
276
+ ) {
277
+ publishProcess(
278
+ processId: $processId
279
+ tag: $tag
280
+ input: { sourceCode: $sourceCode, sourceCodeEncoding: $sourceCodeEncoding, parametersSchema: $parametersSchema, readme: $readme }
281
+ ) {
266
282
  id
267
283
  }
268
284
  }
@@ -271,7 +287,8 @@ class FcodeGraphQLClient {
271
287
  tag: versionedProcess.tag,
272
288
  parametersSchema: versionedProcess.script.parametersSchema,
273
289
  readme: versionedProcess.readme,
274
- sourceCode: versionedProcess.script.sourceCode,
290
+ sourceCode: encodeSourceCode(versionedProcess.script.sourceCode),
291
+ sourceCodeEncoding: SOURCE_CODE_ENCODING,
275
292
  })
276
293
  .catch((error) => {
277
294
  return Promise.reject(this.cleanError(error));
@@ -330,15 +347,20 @@ class FcodeGraphQLClient {
330
347
  createVersionedScriptLibrary(versionedScriptLibrary) {
331
348
  return this.client
332
349
  .request((0, graphql_request_1.gql) `
333
- mutation publishScriptLibrary($scriptLibraryId: ID!, $tag: String!, $sourceCode: String) {
334
- publishScriptLibrary(scriptLibraryId: $scriptLibraryId, tag: $tag, input: { sourceCode: $sourceCode }) {
350
+ mutation publishScriptLibrary($scriptLibraryId: ID!, $tag: String!, $sourceCode: String, $sourceCodeEncoding: SourceCodeEncoding) {
351
+ publishScriptLibrary(
352
+ scriptLibraryId: $scriptLibraryId
353
+ tag: $tag
354
+ input: { sourceCode: $sourceCode, sourceCodeEncoding: $sourceCodeEncoding }
355
+ ) {
335
356
  id
336
357
  }
337
358
  }
338
359
  `, {
339
360
  scriptLibraryId: versionedScriptLibrary.id,
340
361
  tag: versionedScriptLibrary.tag,
341
- sourceCode: versionedScriptLibrary.sourceCode,
362
+ sourceCode: encodeSourceCode(versionedScriptLibrary.sourceCode),
363
+ sourceCodeEncoding: SOURCE_CODE_ENCODING,
342
364
  })
343
365
  .catch((error) => {
344
366
  return Promise.reject(this.cleanError(error));
@@ -553,7 +575,8 @@ class FcodeGraphQLClient {
553
575
  parametersSchema: process.script.parametersSchema,
554
576
  readme: process.readme,
555
577
  tags: process.tags,
556
- sourceCode: process.script.sourceCode,
578
+ sourceCode: encodeSourceCode(process.script.sourceCode),
579
+ sourceCodeEncoding: SOURCE_CODE_ENCODING,
557
580
  manifest: process.settings.dependencies.scopedToProcess ? process.manifest : null,
558
581
  settings,
559
582
  };
@@ -640,8 +663,8 @@ class FcodeGraphQLClient {
640
663
  if (scriptLibrary.id) {
641
664
  return this.client
642
665
  .request((0, graphql_request_1.gql) `
643
- mutation updateScriptLibrary($id: ID!, $sourceCode: String) {
644
- updateScriptLibrary(id: $id, input: { sourceCode: $sourceCode }) {
666
+ mutation updateScriptLibrary($id: ID!, $sourceCode: String, $sourceCodeEncoding: SourceCodeEncoding) {
667
+ updateScriptLibrary(id: $id, input: { sourceCode: $sourceCode, sourceCodeEncoding: $sourceCodeEncoding }) {
645
668
  id
646
669
  name
647
670
  programmingLanguage
@@ -650,7 +673,8 @@ class FcodeGraphQLClient {
650
673
  }
651
674
  `, {
652
675
  id: scriptLibrary.id,
653
- sourceCode: scriptLibrary.sourceCode,
676
+ sourceCode: encodeSourceCode(scriptLibrary.sourceCode),
677
+ sourceCodeEncoding: SOURCE_CODE_ENCODING,
654
678
  })
655
679
  .then(({ updateScriptLibrary: scriptLibrary }) => new types_1.ScriptLibrary(scriptLibrary.id, scriptLibrary.name, scriptLibrary.sourceCode, scriptLibrary.programmingLanguage))
656
680
  .catch((error) => {
@@ -659,8 +683,18 @@ class FcodeGraphQLClient {
659
683
  }
660
684
  return this.client
661
685
  .request((0, graphql_request_1.gql) `
662
- mutation createScriptLibrary($name: String!, $sourceCode: String, $programmingLanguage: ProgrammingLanguage) {
663
- createScriptLibrary(name: $name, programmingLanguage: $programmingLanguage, sourceCode: $sourceCode) {
686
+ mutation createScriptLibrary(
687
+ $name: String!
688
+ $sourceCode: String
689
+ $sourceCodeEncoding: SourceCodeEncoding
690
+ $programmingLanguage: ProgrammingLanguage
691
+ ) {
692
+ createScriptLibrary(
693
+ name: $name
694
+ programmingLanguage: $programmingLanguage
695
+ sourceCode: $sourceCode
696
+ sourceCodeEncoding: $sourceCodeEncoding
697
+ ) {
664
698
  id
665
699
  name
666
700
  sourceCode
@@ -669,7 +703,8 @@ class FcodeGraphQLClient {
669
703
  }
670
704
  `, {
671
705
  name: scriptLibrary.name,
672
- sourceCode: scriptLibrary.sourceCode,
706
+ sourceCode: encodeSourceCode(scriptLibrary.sourceCode),
707
+ sourceCodeEncoding: SOURCE_CODE_ENCODING,
673
708
  programmingLanguage: scriptLibrary.programmingLanguage,
674
709
  })
675
710
  .then(({ createScriptLibrary: scriptLibrary }) => new types_1.ScriptLibrary(scriptLibrary.id, scriptLibrary.name, scriptLibrary.sourceCode, scriptLibrary.programmingLanguage))
@@ -16,7 +16,9 @@ export declare function flattenTranslations(document: unknown, prefix?: string,
16
16
  * publishes plain languages, so the request degrades one segment at a time — split on `-` or `_`,
17
17
  * without parsing a language tag, since any short slug is a legal identifier here — and every step
18
18
  * that matches stays in the chain: a variant file translates the keys that differ and leaves the
19
- * rest to the language it came from.
19
+ * rest to the language it came from. The opposite happens too — the host sends `es` while the
20
+ * workspace has `es-ES` — so a language layer that matches nothing, by name or ignoring case, is
21
+ * broadened to a regional file of that language, in the place the language would have taken.
20
22
  */
21
23
  export declare function localeFallbackChain(requested: string | undefined, primary: string | undefined, available: Array<string>): Array<string>;
22
24
  /**
@@ -25,10 +27,12 @@ export declare function localeFallbackChain(requested: string | undefined, prima
25
27
  * shorter form of the requested one the workspace publishes — layered underneath, plus each
26
28
  * locale's own map (inherited file layered under its own, but NOT merged over the primary) so a
27
29
  * call naming a locale (`fcode.i18n(key, args, { locale })`) resolves like the cloud's `by_locale`:
28
- * key by key in the requested locale first, falling back to the merged translations
30
+ * key by key in the requested locale first, falling back to the merged translations — and the
31
+ * primary locale, which a call naming a bare language prefers when it broadens to a regional file
29
32
  */
30
33
  export declare function resolveLocalTranslations(requestedLocale?: string, workspaceDir?: string): {
31
34
  locale?: string;
35
+ primaryLocale?: string;
32
36
  translations: Record<string, string>;
33
37
  byLocale: Record<string, Record<string, string>>;
34
38
  };
@@ -120,6 +120,20 @@ function matchingLocale(candidate, available) {
120
120
  }
121
121
  return available.filter((locale) => locale.toLowerCase() === candidate.toLowerCase()).sort()[0];
122
122
  }
123
+ const LOCALE_SEPARATOR = /[-_]/;
124
+ /**
125
+ * The available locale a bare language broadens to when the workspace only has regional files for
126
+ * it (`es` → `es-ES`): one whose first segment is the language, case ignored — the primary locale
127
+ * if it is one of them, the alphabetically first otherwise. A language with a separator in it (an
128
+ * identifier that starts with one) broadens to nothing. Mirrors the platform's `LocaleFallbackChain`.
129
+ */
130
+ function sameLanguageLocale(language, primary, available) {
131
+ if (!language || LOCALE_SEPARATOR.test(language)) {
132
+ return undefined;
133
+ }
134
+ const sameLanguage = available.filter((locale) => locale.split(LOCALE_SEPARATOR)[0].toLowerCase() === language.toLowerCase()).sort();
135
+ return primary !== undefined && sameLanguage.includes(primary) ? primary : sameLanguage[0];
136
+ }
123
137
  /**
124
138
  * The locale files that answer a requested identifier, lowest precedence first. Mirrors the
125
139
  * platform's `LocaleFallbackChain`, so a local run and the cloud resolve the same text.
@@ -128,7 +142,9 @@ function matchingLocale(candidate, available) {
128
142
  * publishes plain languages, so the request degrades one segment at a time — split on `-` or `_`,
129
143
  * without parsing a language tag, since any short slug is a legal identifier here — and every step
130
144
  * 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.
145
+ * rest to the language it came from. The opposite happens too — the host sends `es` while the
146
+ * workspace has `es-ES` — so a language layer that matches nothing, by name or ignoring case, is
147
+ * broadened to a regional file of that language, in the place the language would have taken.
132
148
  */
133
149
  function localeFallbackChain(requested, primary, available) {
134
150
  const chain = [];
@@ -146,8 +162,14 @@ function localeFallbackChain(requested, primary, available) {
146
162
  const lastSeparator = Math.max(candidate.lastIndexOf('-'), candidate.lastIndexOf('_'));
147
163
  candidate = lastSeparator > 0 ? candidate.slice(0, lastSeparator) : undefined;
148
164
  }
149
- for (const shorterOrExact of candidates)
150
- add(shorterOrExact);
165
+ candidates.forEach((shorterOrExact, index) => {
166
+ if (index === 0 && !matchingLocale(shorterOrExact, available)) {
167
+ add(sameLanguageLocale(shorterOrExact, primary, available));
168
+ }
169
+ else {
170
+ add(shorterOrExact);
171
+ }
172
+ });
151
173
  return chain;
152
174
  }
153
175
  /**
@@ -156,14 +178,15 @@ function localeFallbackChain(requested, primary, available) {
156
178
  * shorter form of the requested one the workspace publishes — layered underneath, plus each
157
179
  * locale's own map (inherited file layered under its own, but NOT merged over the primary) so a
158
180
  * 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
181
+ * key by key in the requested locale first, falling back to the merged translations — and the
182
+ * primary locale, which a call naming a bare language prefers when it broadens to a regional file
160
183
  */
161
184
  function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd()) {
162
185
  const primary = primaryLocaleOf(workspaceDir);
163
186
  const locale = requestedLocale || primary;
164
187
  const byLocale = Object.fromEntries(availableLocales(workspaceDir).map((slug) => [slug, readLocale(workspaceDir, slug)]));
165
188
  if (!locale) {
166
- return { locale: undefined, translations: {}, byLocale };
189
+ return { locale: undefined, primaryLocale: primary, translations: {}, byLocale };
167
190
  }
168
191
  // `byLocale` already holds every locale's own file, so the merged map — the fallback chain
169
192
  // layered lowest precedence first — is derived from it rather than read from disk a second time.
@@ -171,5 +194,5 @@ function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd())
171
194
  for (const layer of localeFallbackChain(requestedLocale, primary, Object.keys(byLocale))) {
172
195
  Object.assign(translations, byLocale[layer]);
173
196
  }
174
- return { locale, translations, byLocale };
197
+ return { locale, primaryLocale: primary, translations, byLocale };
175
198
  }
@@ -2516,5 +2516,5 @@
2516
2516
  ]
2517
2517
  }
2518
2518
  },
2519
- "version": "3.5.0"
2519
+ "version": "3.5.1"
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.5.1",
4
4
  "packageManager": "pnpm@11.7.0",
5
5
  "description": "Factorial Code Command Line Interface",
6
6
  "homepage": "https://code.factorialhr.com/docs/cli/",