@factorialco/fcode-cli 2.3.0 → 2.4.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.
Files changed (33) hide show
  1. package/README.md +12 -8
  2. package/dist/commands/dependencies/status.d.ts +11 -0
  3. package/dist/commands/dependencies/status.js +43 -0
  4. package/dist/commands/pull.js +1 -1
  5. package/dist/commands/team/pull.d.ts +3 -0
  6. package/dist/commands/team/pull.js +15 -4
  7. package/dist/commands/team/push.d.ts +2 -0
  8. package/dist/commands/team/push.js +55 -4
  9. package/dist/commands/team/status.js +8 -2
  10. package/dist/commands/team/versions/create.d.ts +0 -1
  11. package/dist/commands/team/versions/create.js +2 -11
  12. package/dist/core/dependencies/env-dependencies-command.js +6 -3
  13. package/dist/core/executors/javascript-execution-task.js +14 -5
  14. package/dist/core/types.d.ts +29 -2
  15. package/dist/core/types.js +27 -1
  16. package/dist/core/utils/dependencies.js +4 -1
  17. package/dist/env.js +1 -1
  18. package/dist/repositories/process.js +4 -0
  19. package/dist/repositories/programming-language-manifest.d.ts +17 -1
  20. package/dist/repositories/programming-language-manifest.js +51 -1
  21. package/dist/repositories/team.d.ts +13 -3
  22. package/dist/repositories/team.js +51 -8
  23. package/dist/runtime/libs/python/fcode/context.py +15 -8
  24. package/dist/services/fcode-graphql-client.d.ts +1 -0
  25. package/dist/services/fcode-graphql-client.js +71 -8
  26. package/dist/services/locale-resolver.d.ts +6 -1
  27. package/dist/services/locale-resolver.js +11 -4
  28. package/dist/services/team-versioning.d.ts +16 -0
  29. package/dist/services/team-versioning.js +40 -0
  30. package/dist/utils/gitignore.d.ts +3 -1
  31. package/dist/utils/gitignore.js +11 -2
  32. package/oclif.manifest.json +13 -5
  33. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEPENDENCIES_DIR = exports.INHERITED_FILE_NAME = void 0;
3
4
  const fs = require("fs-extra");
4
5
  const path = require("node:path");
5
6
  const errors_1 = require("../core/errors");
@@ -7,6 +8,15 @@ const types_1 = require("../core/types");
7
8
  const resource_1 = require("./resource");
8
9
  const dependencies_1 = require("../core/utils/dependencies");
9
10
  const file_utils_1 = require("../utils/file-utils");
11
+ /**
12
+ * The inherited file each language collapses its parents into. Exported so the gitignore sync names
13
+ * exactly what this repository writes, rather than repeating the strings.
14
+ */
15
+ exports.INHERITED_FILE_NAME = {
16
+ JAVASCRIPT: 'package.inherited.json',
17
+ PYTHON: 'requirements.inherited.txt',
18
+ };
19
+ exports.DEPENDENCIES_DIR = 'dependencies';
10
20
  class ProgrammingLanguageManifestRepository extends resource_1.default {
11
21
  constructor() {
12
22
  super({
@@ -21,7 +31,7 @@ class ProgrammingLanguageManifestRepository extends resource_1.default {
21
31
  }
22
32
  const sourceCodeFileContent = fs.readFileSync(this.getResourcePath(programmingLanguageManifest));
23
33
  try {
24
- return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.slug, programmingLanguageManifest.programmingLanguage, (0, dependencies_1.dependenciesFromFile)(programmingLanguageManifest.programmingLanguage, sourceCodeFileContent.toString()));
34
+ return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.slug, programmingLanguageManifest.programmingLanguage, (0, dependencies_1.dependenciesFromFile)(programmingLanguageManifest.programmingLanguage, sourceCodeFileContent.toString()), this.readInheritedDependencies(programmingLanguageManifest.programmingLanguage));
25
35
  }
26
36
  catch (error) {
27
37
  throw new errors_1.LocalProgrammingLanguageManifestNotFound(error, slug);
@@ -30,6 +40,25 @@ class ProgrammingLanguageManifestRepository extends resource_1.default {
30
40
  getSourceCodeFileName(programmingLanguageManifest) {
31
41
  return programmingLanguageManifest.programmingLanguage === 'JAVASCRIPT' ? 'package.json' : 'requirements.txt';
32
42
  }
43
+ /**
44
+ * The inherited dependencies of one language, collapsed into a single file the way inherited
45
+ * variables are (`variables.inherited.env`) rather than one file per parent: two versions of the
46
+ * same package cannot be installed side by side, so there is only ever one winner per name and
47
+ * the platform has already picked it.
48
+ */
49
+ getInheritedFileName(programmingLanguage) {
50
+ return exports.INHERITED_FILE_NAME[programmingLanguage];
51
+ }
52
+ getInheritedPath(programmingLanguage) {
53
+ return path.join(this._repositoryName, this.getInheritedFileName(programmingLanguage));
54
+ }
55
+ readInheritedDependencies(programmingLanguage) {
56
+ const inheritedPath = this.getInheritedPath(programmingLanguage);
57
+ if (!fs.existsSync(inheritedPath)) {
58
+ return [];
59
+ }
60
+ return (0, dependencies_1.dependenciesFromFile)(programmingLanguage, fs.readFileSync(inheritedPath).toString());
61
+ }
33
62
  getResourceDirectory(programmingLanguageManifest) {
34
63
  return this._repositoryName;
35
64
  }
@@ -40,6 +69,7 @@ class ProgrammingLanguageManifestRepository extends resource_1.default {
40
69
  await new Promise((resolve, reject) => {
41
70
  try {
42
71
  fs.removeSync(this.getResourcePath(programmingLanguageManifest));
72
+ this.removeInheritedFile(programmingLanguageManifest.programmingLanguage);
43
73
  this.removeDirectoryIfEmpty(this.getResourceDirectory(programmingLanguageManifest));
44
74
  resolve(true);
45
75
  }
@@ -57,6 +87,26 @@ class ProgrammingLanguageManifestRepository extends resource_1.default {
57
87
  }
58
88
  async saveFiles(programmingLanguageManifest) {
59
89
  (0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(programmingLanguageManifest), (0, dependencies_1.dependenciesToFile)(programmingLanguageManifest.programmingLanguage, programmingLanguageManifest.dependencies));
90
+ const { programmingLanguage, inheritedDependencies } = programmingLanguageManifest;
91
+ if (inheritedDependencies.length === 0) {
92
+ // The workspace stopped inheriting anything (a parent was removed, or emptied its manifest):
93
+ // a stale file would keep being installed locally and keep showing in `status`.
94
+ this.removeInheritedFile(programmingLanguage);
95
+ return;
96
+ }
97
+ // Read-only, like inherited locales and modules: these belong to a parent workspace, and
98
+ // editing one here would change nothing — declare the name in package.json to override it.
99
+ (0, file_utils_1.outputFileSyncIfChanged)(this.getInheritedPath(programmingLanguage), (0, dependencies_1.dependenciesToFile)(programmingLanguage, inheritedDependencies), {
100
+ readOnly: true,
101
+ });
102
+ }
103
+ removeInheritedFile(programmingLanguage) {
104
+ const inheritedPath = this.getInheritedPath(programmingLanguage);
105
+ if (fs.existsSync(inheritedPath)) {
106
+ // Written read-only, so it has to be made writable before it can go.
107
+ fs.chmodSync(inheritedPath, 0o644);
108
+ fs.removeSync(inheritedPath);
109
+ }
60
110
  }
61
111
  async metadataFromFiles(slug) {
62
112
  if (slug === 'JAVASCRIPT') {
@@ -17,14 +17,24 @@ export default class TeamRepository {
17
17
  /** Builds a Team from the cloud's raw settings, resolving the error-handler id to a slug. */
18
18
  fromRaw(raw: RawTeamSettings): Promise<Team>;
19
19
  /**
20
- * Writes the cloud's team versions and aliases into the Team as the pull-only team.json
21
- * fields (sorted for stable git diffs). These fields never enter the content hash and are
22
- * stripped from pushes: all writes go through the team:versions/team:aliases commands.
20
+ * Writes the cloud's team versions and aliases into the Team as the team.json versioning
21
+ * fields (sorted for stable git diffs). Cloud entries always carry a createdAt; only
22
+ * hand-added local entries lack one, and those never pass through here.
23
23
  */
24
24
  applyCloudVersioning(team: Team, versions: TeamVersion[], aliases: TeamVersionAlias[]): void;
25
25
  resolveSlugToId(processSlug: string): Promise<string>;
26
26
  resolveIdToSlug(processId: string): Promise<string>;
27
27
  private parseErrorHandler;
28
+ /**
29
+ * team.json is hand-edited to declare versions, so entries without a tag are dropped and a
30
+ * duplicated tag keeps its first entry (a tag names one immutable snapshot).
31
+ */
32
+ private parseVersions;
33
+ /**
34
+ * An alias is a name->tag pointer; entries missing either half are dropped and a duplicated
35
+ * name keeps its first entry.
36
+ */
37
+ private parseAliases;
28
38
  /** A configuration without a variable key is meaningless, so it is read as absent. */
29
39
  private parseWebhookAuth;
30
40
  private toFile;
@@ -30,12 +30,11 @@ class TeamRepository {
30
30
  catch { }
31
31
  const parentTeamSlugs = Array.isArray(raw.parentTeamSlugs) ? raw.parentTeamSlugs : [];
32
32
  const team = new types_1.Team(slug, parentTeamSlugs, raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig), this.parseWebhookAuth(raw.webhookAuth), typeof raw.primaryLocale === 'string' && raw.primaryLocale.length > 0 ? raw.primaryLocale : undefined);
33
- // Parsed so a find()+save() round-trip does not drop the pull-only fields.
34
33
  if (Array.isArray(raw.versions)) {
35
- team.versions = raw.versions;
34
+ team.versions = this.parseVersions(raw.versions);
36
35
  }
37
36
  if (Array.isArray(raw.aliases)) {
38
- team.aliases = raw.aliases;
37
+ team.aliases = this.parseAliases(raw.aliases);
39
38
  }
40
39
  return team;
41
40
  }
@@ -54,9 +53,9 @@ class TeamRepository {
54
53
  return new types_1.Team(raw.slug, raw.parentTeamSlugs ?? [], raw.zoneId, errorHandlerConfig, this.parseWebhookAuth(raw.webhookAuth), raw.primaryLocale ?? undefined);
55
54
  }
56
55
  /**
57
- * Writes the cloud's team versions and aliases into the Team as the pull-only team.json
58
- * fields (sorted for stable git diffs). These fields never enter the content hash and are
59
- * stripped from pushes: all writes go through the team:versions/team:aliases commands.
56
+ * Writes the cloud's team versions and aliases into the Team as the team.json versioning
57
+ * fields (sorted for stable git diffs). Cloud entries always carry a createdAt; only
58
+ * hand-added local entries lack one, and those never pass through here.
60
59
  */
61
60
  applyCloudVersioning(team, versions, aliases) {
62
61
  team.versions = versions
@@ -67,7 +66,7 @@ class TeamRepository {
67
66
  }
68
67
  return entry;
69
68
  })
70
- .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
69
+ .sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? ''));
71
70
  team.aliases = aliases
72
71
  .map((alias) => ({ name: alias.name, tag: alias.tag }))
73
72
  .sort((a, b) => a.name.localeCompare(b.name));
@@ -87,6 +86,50 @@ class TeamRepository {
87
86
  }
88
87
  return { processSlug: raw.processSlug, tag: raw.tag ?? null };
89
88
  }
89
+ /**
90
+ * team.json is hand-edited to declare versions, so entries without a tag are dropped and a
91
+ * duplicated tag keeps its first entry (a tag names one immutable snapshot).
92
+ */
93
+ parseVersions(raw) {
94
+ const versions = [];
95
+ for (const candidate of raw) {
96
+ if (!candidate || typeof candidate.tag !== 'string' || candidate.tag.length === 0) {
97
+ continue;
98
+ }
99
+ if (versions.some(({ tag }) => tag === candidate.tag)) {
100
+ continue;
101
+ }
102
+ const entry = { tag: candidate.tag };
103
+ if (typeof candidate.comment === 'string' && candidate.comment.length > 0) {
104
+ entry.comment = candidate.comment;
105
+ }
106
+ if (typeof candidate.createdAt === 'string' && candidate.createdAt.length > 0) {
107
+ entry.createdAt = candidate.createdAt;
108
+ }
109
+ versions.push(entry);
110
+ }
111
+ return versions;
112
+ }
113
+ /**
114
+ * An alias is a name->tag pointer; entries missing either half are dropped and a duplicated
115
+ * name keeps its first entry.
116
+ */
117
+ parseAliases(raw) {
118
+ const aliases = [];
119
+ for (const candidate of raw) {
120
+ if (!candidate || typeof candidate.name !== 'string' || candidate.name.length === 0) {
121
+ continue;
122
+ }
123
+ if (typeof candidate.tag !== 'string' || candidate.tag.length === 0) {
124
+ continue;
125
+ }
126
+ if (aliases.some(({ name }) => name === candidate.name)) {
127
+ continue;
128
+ }
129
+ aliases.push({ name: candidate.name, tag: candidate.tag });
130
+ }
131
+ return aliases;
132
+ }
90
133
  /** A configuration without a variable key is meaningless, so it is read as absent. */
91
134
  parseWebhookAuth(raw) {
92
135
  if (!raw || typeof raw.variableKey !== 'string' || raw.variableKey.length === 0) {
@@ -114,7 +157,7 @@ class TeamRepository {
114
157
  if (team.primaryLocale !== undefined) {
115
158
  file.primaryLocale = team.primaryLocale;
116
159
  }
117
- // Pull-only global versioning fields, appended after the pushable settings.
160
+ // Global versioning fields, appended after the scalar settings.
118
161
  if (team.versions !== undefined) {
119
162
  file.versions = team.versions;
120
163
  }
@@ -81,22 +81,29 @@ class Processes(dict):
81
81
  class I18n:
82
82
  """Local equivalent of the cloud's fcode.i18n.
83
83
 
84
- The CLI has already layered the chosen locale over the workspace's primary one, so what
85
- arrives here is a single flat map. Nothing raises: a key with no translation resolves to the
86
- key itself, and a placeholder with no argument is left exactly as written.
87
-
88
- The ``options`` argument (the version a call pins in the cloud) is accepted and ignored: a
89
- workspace directory holds no published snapshots, so a pinned call resolves against the files
90
- on disk -- the same per-file fallback the cloud applies to a locale never published at that tag.
84
+ The CLI has already layered the chosen locale over the workspace's primary one, so the base
85
+ map that arrives here is flat. ``byLocale`` carries each locale's own files, so a call naming
86
+ a locale (``{"locale": "es"}``) resolves against that locale first and falls back key by key
87
+ to the base map, exactly like the cloud executors. Nothing raises: a key with no translation
88
+ resolves to the key itself, and a placeholder with no argument is left exactly as written.
89
+
90
+ The version a call pins in the cloud is accepted and ignored: a workspace directory holds no
91
+ published snapshots, so a pinned call resolves against the files on disk -- the same per-file
92
+ fallback the cloud applies to a locale never published at that tag.
91
93
  """
92
94
 
93
95
  def __init__(self, i18n_payload):
94
96
  payload = i18n_payload or {}
95
97
  self.__translations = payload.get("translations") or {}
98
+ self.__by_locale = payload.get("byLocale") or {}
96
99
  self.locale = payload.get("locale")
97
100
 
98
101
  def __call__(self, key, args=None, options=None):
99
- template = self.__translations.get(key)
102
+ locale = options.get("locale") if isinstance(options, dict) else None
103
+ locale_map = self.__by_locale.get(str(locale)) if locale is not None else None
104
+ template = locale_map.get(key) if locale_map is not None else None
105
+ if template is None:
106
+ template = self.__translations.get(key)
100
107
  if template is None:
101
108
  return key
102
109
  if not isinstance(args, dict):
@@ -68,6 +68,7 @@ declare class FcodeGraphQLClient {
68
68
  team(): Promise<RawTeamSettings>;
69
69
  pushTeam(update: TeamUpdateInput): Promise<RawTeamSettings>;
70
70
  mapProgrammingLanguageManifest(programmingLanguageManifest: any): ProgrammingLanguageManifest;
71
+ private mapDependencies;
71
72
  allProgrammingLanguageManifest(): Promise<Array<ProgrammingLanguageManifest>>;
72
73
  pushProgrammingLanguageManifest(programmingLanguageManifest: ProgrammingLanguageManifest): Promise<ProgrammingLanguageManifest>;
73
74
  allServiceAccounts(): Promise<Array<ServiceAccount>>;
@@ -4,6 +4,10 @@ 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
+ // Manifest fields an fcode-api older than this CLI may not know. The manifest query falls back to a
8
+ // narrower selection when the error names one of them, so a CLI stays usable against an
9
+ // on-premises api that has not been updated yet.
10
+ const UNKNOWN_MANIFEST_FIELDS = ['isNative', 'inheritedDependencies'];
7
11
  // Shared team version selections so every query/mutation returns the same shape.
8
12
  const TEAM_VERSION_FIELDS = `
9
13
  id
@@ -67,6 +71,13 @@ const PROCESS_FIELDS = `
67
71
  appRole
68
72
  authMode
69
73
  }
74
+ uiTrigger {
75
+ enabled
76
+ locationId
77
+ label
78
+ icon
79
+ awaitResult
80
+ }
70
81
  dependencies {
71
82
  scopedToProcess
72
83
  }
@@ -429,6 +440,21 @@ class FcodeGraphQLClient {
429
440
  settings.formSettings.authMode = process.form.authMode;
430
441
  }
431
442
  }
443
+ if (process.uiTrigger) {
444
+ settings.uiTrigger = { enabled: process.uiTrigger.enabled };
445
+ if (process.uiTrigger.locationId) {
446
+ settings.uiTrigger.locationId = process.uiTrigger.locationId;
447
+ }
448
+ if (process.uiTrigger.label) {
449
+ settings.uiTrigger.label = process.uiTrigger.label;
450
+ }
451
+ if (process.uiTrigger.icon) {
452
+ settings.uiTrigger.icon = process.uiTrigger.icon;
453
+ }
454
+ if (process.uiTrigger.awaitResult !== undefined) {
455
+ settings.uiTrigger.awaitResult = process.uiTrigger.awaitResult;
456
+ }
457
+ }
432
458
  const input = {
433
459
  name: process.name,
434
460
  parametersSchema: process.script.parametersSchema,
@@ -645,6 +671,13 @@ class FcodeGraphQLClient {
645
671
  isNative
646
672
  gitUrl
647
673
  }
674
+ inheritedDependencies {
675
+ name
676
+ semVer
677
+ isNative
678
+ gitUrl
679
+ resolvingTeamSlug
680
+ }
648
681
  }
649
682
  }
650
683
  `, { programmingLanguage })
@@ -654,7 +687,10 @@ class FcodeGraphQLClient {
654
687
  .catch((error) => {
655
688
  // TODO: This is a workaround backward compatibility with old versions of fcode-api.
656
689
  // Remove when full on-premises installations are updated.
657
- if (error.response && error.response.errors && error.response.errors[0] && error.response.errors[0].message.includes('isNative')) {
690
+ // `inheritedDependencies` is listed for the same reason `isNative` is: an api predating
691
+ // dependency inheritance rejects the whole query over the unknown field, and the fallback
692
+ // below simply reads no inherited dependencies.
693
+ if (error.response?.errors?.[0]?.message && UNKNOWN_MANIFEST_FIELDS.some((field) => error.response.errors[0].message.includes(field))) {
658
694
  return this.client
659
695
  .request((0, graphql_request_1.gql) `
660
696
  query getProgrammingLanguageManifest($programmingLanguage: ProgrammingLanguage!) {
@@ -732,6 +768,21 @@ class FcodeGraphQLClient {
732
768
  metadata.form.authMode = settings.formSettings.authMode;
733
769
  }
734
770
  }
771
+ if (settings?.uiTrigger) {
772
+ metadata.uiTrigger = { enabled: Boolean(settings.uiTrigger.enabled) };
773
+ if (settings.uiTrigger.locationId) {
774
+ metadata.uiTrigger.locationId = settings.uiTrigger.locationId;
775
+ }
776
+ if (settings.uiTrigger.label) {
777
+ metadata.uiTrigger.label = settings.uiTrigger.label;
778
+ }
779
+ if (settings.uiTrigger.icon) {
780
+ metadata.uiTrigger.icon = settings.uiTrigger.icon;
781
+ }
782
+ if (settings.uiTrigger.awaitResult) {
783
+ metadata.uiTrigger.awaitResult = true;
784
+ }
785
+ }
735
786
  return new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug));
736
787
  }
737
788
  /**
@@ -941,15 +992,23 @@ class FcodeGraphQLClient {
941
992
  }
942
993
  mapProgrammingLanguageManifest(programmingLanguageManifest) {
943
994
  const allDependencies = programmingLanguageManifest.nextInstallation?.dependencies || programmingLanguageManifest.dependencies;
944
- const dependencies = _.map(allDependencies, (dependency) => {
945
- if (!dependency.isNative) {
946
- return { name: dependency.name, specifier: dependency.gitUrl ?? dependency.semVer };
995
+ return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.programmingLanguage, programmingLanguageManifest.programmingLanguage, this.mapDependencies(allDependencies),
996
+ // Absent when the api predates dependency inheritance, or when the workspace has no parents.
997
+ this.mapDependencies(programmingLanguageManifest.inheritedDependencies));
998
+ }
999
+ mapDependencies(dependencies) {
1000
+ return _.map(dependencies, (dependency) => {
1001
+ if (dependency.isNative) {
1002
+ return null;
947
1003
  }
948
- return null;
1004
+ return {
1005
+ name: dependency.name,
1006
+ specifier: dependency.gitUrl ?? dependency.semVer,
1007
+ ...(dependency.resolvingTeamSlug && { resolvingTeamSlug: dependency.resolvingTeamSlug }),
1008
+ };
949
1009
  })
950
1010
  .filter(Boolean)
951
- .sort((a, b) => a && b && a.name.localeCompare(b.name));
952
- return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.programmingLanguage, programmingLanguageManifest.programmingLanguage, dependencies);
1011
+ .sort((a, b) => a.name.localeCompare(b.name));
953
1012
  }
954
1013
  allProgrammingLanguageManifest() {
955
1014
  const result = [];
@@ -983,7 +1042,11 @@ class FcodeGraphQLClient {
983
1042
  }
984
1043
  `, {
985
1044
  id: programmingLanguageManifest.id,
986
- input: { dependencies: programmingLanguageManifest.dependencies },
1045
+ input: {
1046
+ // Only name and specifier: DependencyUpdateInput has no other fields, and a workspace
1047
+ // pushes its own list — inherited entries live in a separate file and never get here.
1048
+ dependencies: programmingLanguageManifest.dependencies.map(({ name, specifier }) => ({ name, specifier })),
1049
+ },
987
1050
  })
988
1051
  .then(({ updateProgrammingLanguageManifest: programmingLanguageManifest }) => {
989
1052
  return this.client
@@ -10,9 +10,14 @@
10
10
  export declare function flattenTranslations(document: unknown, prefix?: string, target?: Record<string, string>): Record<string, string>;
11
11
  /**
12
12
  * @param requestedLocale the locale to run with, or undefined to use the workspace's primary
13
- * @returns the locale actually used and its flat translations, with the primary locale underneath
13
+ * @returns the locale actually used and its flat translations, with the primary locale underneath,
14
+ * plus each locale's own map (inherited file layered under its own, but NOT merged over the
15
+ * primary) so a call naming a locale (`fcode.i18n(key, args, { locale })`) resolves like the
16
+ * cloud's `by_locale`: key by key in the requested locale first, falling back to the merged
17
+ * translations
14
18
  */
15
19
  export declare function resolveLocalTranslations(requestedLocale?: string, workspaceDir?: string): {
16
20
  locale?: string;
17
21
  translations: Record<string, string>;
22
+ byLocale: Record<string, Record<string, string>>;
18
23
  };
@@ -107,14 +107,21 @@ function primaryLocaleOf(workspaceDir) {
107
107
  }
108
108
  /**
109
109
  * @param requestedLocale the locale to run with, or undefined to use the workspace's primary
110
- * @returns the locale actually used and its flat translations, with the primary locale underneath
110
+ * @returns the locale actually used and its flat translations, with the primary locale underneath,
111
+ * plus each locale's own map (inherited file layered under its own, but NOT merged over the
112
+ * primary) so a call naming a locale (`fcode.i18n(key, args, { locale })`) resolves like the
113
+ * cloud's `by_locale`: key by key in the requested locale first, falling back to the merged
114
+ * translations
111
115
  */
112
116
  function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd()) {
113
117
  const primary = primaryLocaleOf(workspaceDir);
114
118
  const locale = requestedLocale || primary;
119
+ const byLocale = Object.fromEntries(availableLocales(workspaceDir).map((slug) => [slug, readLocale(workspaceDir, slug)]));
115
120
  if (!locale) {
116
- return { locale: undefined, translations: {} };
121
+ return { locale: undefined, translations: {}, byLocale };
117
122
  }
118
- const fallback = primary && primary !== locale ? readLocale(workspaceDir, primary) : {};
119
- return { locale, translations: { ...fallback, ...readLocale(workspaceDir, locale) } };
123
+ // `byLocale` already holds every locale's own file, so the merged map — the requested locale
124
+ // layered over the primary — is derived from it rather than read from disk a second time.
125
+ const fallback = primary ? byLocale[primary] : {};
126
+ return { locale, translations: { ...fallback, ...(byLocale[locale] ?? {}) }, byLocale };
120
127
  }
@@ -0,0 +1,16 @@
1
+ import { Team, TeamAliasFileEntry, TeamVersion, TeamVersionAlias, TeamVersionFileEntry } from '../core/types';
2
+ /**
3
+ * What a team:push must do to make the cloud's team versions and aliases match team.json.
4
+ * Versions are only ever created or deleted (tags are immutable snapshots); aliases are
5
+ * name->tag pointers, so a changed tag is an upsert of the same name.
6
+ */
7
+ export interface TeamVersioningSyncPlan {
8
+ createVersions: TeamVersionFileEntry[];
9
+ deleteVersions: TeamVersion[];
10
+ setAliases: TeamAliasFileEntry[];
11
+ deleteAliases: TeamVersionAlias[];
12
+ }
13
+ export declare function planTeamVersioningSync(team: Team, cloudVersions: TeamVersion[], cloudAliases: TeamVersionAlias[]): TeamVersioningSyncPlan;
14
+ export declare function teamVersioningSyncPlanIsEmpty(plan: TeamVersioningSyncPlan): boolean;
15
+ /** Logs the per-entity outcome of publishing a team version and returns how many failed. */
16
+ export declare function logTeamVersionManifest(teamVersion: TeamVersion): number;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planTeamVersioningSync = planTeamVersioningSync;
4
+ exports.teamVersioningSyncPlanIsEmpty = teamVersioningSyncPlanIsEmpty;
5
+ exports.logTeamVersionManifest = logTeamVersionManifest;
6
+ const color_1 = require("@oclif/color");
7
+ const cli_1 = require("../utils/cli");
8
+ function planTeamVersioningSync(team, cloudVersions, cloudAliases) {
9
+ const localVersions = team.versions ?? [];
10
+ const localAliases = team.aliases ?? [];
11
+ const cloudVersionTags = new Set(cloudVersions.map(({ tag }) => tag));
12
+ const localVersionTags = new Set(localVersions.map(({ tag }) => tag));
13
+ const cloudAliasesByName = new Map(cloudAliases.map((alias) => [alias.name, alias]));
14
+ const localAliasNames = new Set(localAliases.map(({ name }) => name));
15
+ return {
16
+ createVersions: localVersions.filter(({ tag }) => !cloudVersionTags.has(tag)),
17
+ deleteVersions: cloudVersions.filter(({ tag }) => !localVersionTags.has(tag)),
18
+ setAliases: localAliases.filter(({ name, tag }) => cloudAliasesByName.get(name)?.tag !== tag),
19
+ deleteAliases: cloudAliases.filter(({ name }) => !localAliasNames.has(name)),
20
+ };
21
+ }
22
+ function teamVersioningSyncPlanIsEmpty(plan) {
23
+ return (plan.createVersions.length === 0 &&
24
+ plan.deleteVersions.length === 0 &&
25
+ plan.setAliases.length === 0 &&
26
+ plan.deleteAliases.length === 0);
27
+ }
28
+ /** Logs the per-entity outcome of publishing a team version and returns how many failed. */
29
+ function logTeamVersionManifest(teamVersion) {
30
+ const created = teamVersion.manifest.filter(({ outcome }) => outcome === 'CREATED');
31
+ const skipped = teamVersion.manifest.filter(({ outcome }) => outcome === 'SKIPPED_EXISTS');
32
+ const failed = teamVersion.manifest.filter(({ outcome }) => outcome === 'FAILED');
33
+ cli_1.default.log(color_1.default.green(`✅ ${created.length} created`) +
34
+ `, ${skipped.length} skipped (tag already exists)` +
35
+ (failed.length > 0 ? `, ${color_1.default.red(`${failed.length} failed`)}` : ''));
36
+ for (const entry of failed) {
37
+ cli_1.default.error(`${entry.resourceType.toLowerCase()} ${entry.slug} — ${entry.error ?? 'unknown error'}`);
38
+ }
39
+ return failed.length;
40
+ }
@@ -1,5 +1,6 @@
1
1
  export declare const INHERITED_BLOCK_START = "# BEGIN fcode inherited resources (auto-generated, do not edit)";
2
2
  export declare const INHERITED_BLOCK_END = "# END fcode inherited resources";
3
+ export declare const INHERITED_DEPENDENCY_FILES: string[];
3
4
  /**
4
5
  * Rebuilds the managed "inherited resources" block in a .gitignore.
5
6
  *
@@ -14,6 +15,7 @@ export declare function renderGitignoreWithInherited(existing: string, inherited
14
15
  * parent team and pulled read-only) are not tracked by git. Regenerated from the
15
16
  * current workspace metadata, so resources that stop being inherited drop out.
16
17
  * Inherited variable values live in one file, which is ignored as soon as the
17
- * workspace inherits any variable; inherited locales are one file each, like modules.
18
+ * workspace inherits any variable; inherited locales are one file each, like modules;
19
+ * inherited dependencies are one file per language, like variables.
18
20
  */
19
21
  export declare function syncInheritedGitignore(workspaceDir?: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.INHERITED_BLOCK_END = exports.INHERITED_BLOCK_START = void 0;
3
+ exports.INHERITED_DEPENDENCY_FILES = exports.INHERITED_BLOCK_END = exports.INHERITED_BLOCK_START = void 0;
4
4
  exports.renderGitignoreWithInherited = renderGitignoreWithInherited;
5
5
  exports.syncInheritedGitignore = syncInheritedGitignore;
6
6
  const path = require("node:path");
@@ -8,10 +8,14 @@ const fs = require("fs-extra");
8
8
  const workspace_1 = require("../config/workspace");
9
9
  const global_1 = require("../config/global");
10
10
  const settings_1 = require("../config/settings");
11
+ const programming_language_manifest_1 = require("../repositories/programming-language-manifest");
11
12
  const filenamify_1 = require("./filenamify");
12
13
  const file_utils_1 = require("./file-utils");
13
14
  exports.INHERITED_BLOCK_START = '# BEGIN fcode inherited resources (auto-generated, do not edit)';
14
15
  exports.INHERITED_BLOCK_END = '# END fcode inherited resources';
16
+ // Derived from the repository that writes them, so renaming a file cannot silently stop it being
17
+ // gitignored.
18
+ exports.INHERITED_DEPENDENCY_FILES = Object.values(programming_language_manifest_1.INHERITED_FILE_NAME).map((name) => `${programming_language_manifest_1.DEPENDENCIES_DIR}/${name}`);
15
19
  /**
16
20
  * Rebuilds the managed "inherited resources" block in a .gitignore.
17
21
  *
@@ -46,7 +50,8 @@ function stripManagedBlock(content) {
46
50
  * parent team and pulled read-only) are not tracked by git. Regenerated from the
47
51
  * current workspace metadata, so resources that stop being inherited drop out.
48
52
  * Inherited variable values live in one file, which is ignored as soon as the
49
- * workspace inherits any variable; inherited locales are one file each, like modules.
53
+ * workspace inherits any variable; inherited locales are one file each, like modules;
54
+ * inherited dependencies are one file per language, like variables.
50
55
  */
51
56
  async function syncInheritedGitignore(workspaceDir = process.cwd()) {
52
57
  const team = await new workspace_1.default(workspaceDir).getActiveTeam();
@@ -62,6 +67,10 @@ async function syncInheritedGitignore(workspaceDir = process.cwd()) {
62
67
  ...Object.values(team.i18n ?? {})
63
68
  .filter((locale) => locale.resolvingTeamSlug)
64
69
  .map((locale) => `i18n/${(0, filenamify_1.default)(locale.slug)}.yaml`),
70
+ // Inherited dependencies are one file per language, written only when there is something to
71
+ // inherit — checked on disk because the manifest metadata tracks the workspace's own list, not
72
+ // what its parents contribute.
73
+ ...exports.INHERITED_DEPENDENCY_FILES.filter((file) => fs.existsSync(path.join(workspaceDir, file))),
65
74
  ];
66
75
  const gitignorePath = path.join(workspaceDir, '.gitignore');
67
76
  const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
@@ -1675,11 +1675,19 @@
1675
1675
  "team:pull": {
1676
1676
  "aliases": [],
1677
1677
  "args": {},
1678
- "description": "Fetch team settings (timezone, error handler, inheritance) from cloud into team.json",
1678
+ "description": "Fetch team settings (timezone, error handler, inheritance) and team versions/aliases from cloud into team.json",
1679
1679
  "examples": [
1680
1680
  "$ fcode team:pull"
1681
1681
  ],
1682
- "flags": {},
1682
+ "flags": {
1683
+ "force": {
1684
+ "char": "f",
1685
+ "description": "Force command",
1686
+ "name": "force",
1687
+ "allowNo": false,
1688
+ "type": "boolean"
1689
+ }
1690
+ },
1683
1691
  "hasDynamicHelp": false,
1684
1692
  "hiddenAliases": [],
1685
1693
  "id": "team:pull",
@@ -1698,7 +1706,7 @@
1698
1706
  "team:push": {
1699
1707
  "aliases": [],
1700
1708
  "args": {},
1701
- "description": "Update cloud team settings (timezone, error handler, inheritance, webhook auth) from team.json",
1709
+ "description": "Update cloud team settings (timezone, error handler, inheritance, webhook auth) and sync team versions/aliases from team.json",
1702
1710
  "examples": [
1703
1711
  "$ fcode team:push"
1704
1712
  ],
@@ -1729,7 +1737,7 @@
1729
1737
  "team:status": {
1730
1738
  "aliases": [],
1731
1739
  "args": {},
1732
- "description": "Show whether team settings (timezone, error handler, inheritance) differ from the cloud",
1740
+ "description": "Show whether team settings (timezone, error handler, inheritance) or team versions/aliases differ from the cloud",
1733
1741
  "examples": [
1734
1742
  "$ fcode team:status"
1735
1743
  ],
@@ -2163,5 +2171,5 @@
2163
2171
  ]
2164
2172
  }
2165
2173
  },
2166
- "version": "2.3.0"
2174
+ "version": "2.4.0"
2167
2175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorialco/fcode-cli",
3
- "version": "2.3.0",
3
+ "version": "2.4.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/",