@factorialco/fcode-cli 2.2.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 (46) hide show
  1. package/README.md +12 -8
  2. package/dist/commands/clone.js +14 -1
  3. package/dist/commands/dependencies/status.d.ts +11 -0
  4. package/dist/commands/dependencies/status.js +43 -0
  5. package/dist/commands/pull.js +1 -1
  6. package/dist/commands/team/pull.d.ts +3 -0
  7. package/dist/commands/team/pull.js +15 -4
  8. package/dist/commands/team/push.d.ts +2 -0
  9. package/dist/commands/team/push.js +55 -4
  10. package/dist/commands/team/status.js +8 -2
  11. package/dist/commands/team/versions/create.d.ts +0 -1
  12. package/dist/commands/team/versions/create.js +2 -11
  13. package/dist/core/commands/api-client-command.d.ts +1 -0
  14. package/dist/core/commands/api-client-command.js +5 -0
  15. package/dist/core/commands/base-command.d.ts +2 -0
  16. package/dist/core/commands/base-command.js +31 -0
  17. package/dist/core/dependencies/env-dependencies-command.js +6 -3
  18. package/dist/core/errors.d.ts +4 -0
  19. package/dist/core/errors.js +16 -1
  20. package/dist/core/executors/javascript-execution-task.js +14 -5
  21. package/dist/core/types.d.ts +29 -2
  22. package/dist/core/types.js +30 -2
  23. package/dist/core/utils/dependencies.js +4 -1
  24. package/dist/env.js +1 -1
  25. package/dist/repositories/process.js +4 -0
  26. package/dist/repositories/programming-language-manifest.d.ts +17 -1
  27. package/dist/repositories/programming-language-manifest.js +51 -1
  28. package/dist/repositories/team.d.ts +13 -3
  29. package/dist/repositories/team.js +51 -8
  30. package/dist/runtime/libs/python/fcode/context.py +15 -8
  31. package/dist/services/auth-service.d.ts +1 -0
  32. package/dist/services/auth-service.js +12 -0
  33. package/dist/services/fcode-graphql-client.d.ts +3 -0
  34. package/dist/services/fcode-graphql-client.js +97 -9
  35. package/dist/services/locale-merger.d.ts +1 -1
  36. package/dist/services/locale-merger.js +16 -13
  37. package/dist/services/locale-resolver.d.ts +6 -1
  38. package/dist/services/locale-resolver.js +11 -4
  39. package/dist/services/team-access.d.ts +13 -0
  40. package/dist/services/team-access.js +45 -0
  41. package/dist/services/team-versioning.d.ts +16 -0
  42. package/dist/services/team-versioning.js +40 -0
  43. package/dist/utils/gitignore.d.ts +3 -1
  44. package/dist/utils/gitignore.js +11 -2
  45. package/oclif.manifest.json +13 -5
  46. package/package.json +1 -1
@@ -130,12 +130,30 @@ export interface ProcessForm {
130
130
  appRole?: AppRole;
131
131
  authMode?: FormAuthMode;
132
132
  }
133
+ export interface ProcessUiTrigger {
134
+ enabled: boolean;
135
+ /** Identifier of the UI location the trigger is rendered in. */
136
+ locationId?: string;
137
+ /**
138
+ * Label shown on the trigger. May contain fcode.i18n("key") tokens, which the platform
139
+ * resolves when a process listing names a locale.
140
+ */
141
+ label?: string;
142
+ /** f0 icon name, e.g. "Globe". */
143
+ icon?: string;
144
+ /**
145
+ * When true the trigger executes the process synchronously and returns the result
146
+ * envelope; otherwise the execution is fire-and-forget.
147
+ */
148
+ awaitResult?: boolean;
149
+ }
133
150
  export interface ProcessMetadataFields {
134
151
  name: string;
135
152
  description?: string;
136
153
  tags: string[];
137
154
  webhook?: ProcessWebhook;
138
155
  form?: ProcessForm;
156
+ uiTrigger?: ProcessUiTrigger;
139
157
  }
140
158
  export declare class Process extends Resource {
141
159
  id: string;
@@ -146,6 +164,7 @@ export declare class Process extends Resource {
146
164
  tags: string[];
147
165
  webhook?: ProcessWebhook;
148
166
  form?: ProcessForm;
167
+ uiTrigger?: ProcessUiTrigger;
149
168
  script: {
150
169
  parametersSchema: any;
151
170
  programmingLanguage: ProgrammingLanguage;
@@ -238,7 +257,14 @@ export declare class ProgrammingLanguageManifest extends Resource {
238
257
  slug: string;
239
258
  programmingLanguage: ProgrammingLanguage;
240
259
  dependencies: Array<Dependency>;
241
- constructor(id: string, slug: string, programmingLanguage: ProgrammingLanguage, dependencies: Array<Dependency>);
260
+ inheritedDependencies: Array<Dependency>;
261
+ constructor(id: string, slug: string, programmingLanguage: ProgrammingLanguage, dependencies: Array<Dependency>, inheritedDependencies?: Array<Dependency>);
262
+ /**
263
+ * What an execution installs: this workspace's dependencies layered over the inherited ones, one
264
+ * entry per package name. Mirrors the platform's merge so a local run resolves the same install
265
+ * directory the cloud does.
266
+ */
267
+ get effectiveDependencies(): Array<Dependency>;
242
268
  get contentHash(): string;
243
269
  }
244
270
  export interface ErrorHandlerConfig {
@@ -273,7 +299,7 @@ export interface TeamVersionAlias {
273
299
  export interface TeamVersionFileEntry {
274
300
  tag: string;
275
301
  comment?: string;
276
- createdAt: string;
302
+ createdAt?: string;
277
303
  }
278
304
  export interface TeamAliasFileEntry {
279
305
  name: string;
@@ -363,6 +389,7 @@ export declare const DependenciesFlags: {
363
389
  export declare class Dependency {
364
390
  name: string;
365
391
  specifier: string;
392
+ resolvingTeamSlug?: string;
366
393
  }
367
394
  export declare class ServiceAccount extends Resource {
368
395
  id: string;
@@ -49,6 +49,7 @@ class Process extends Resource {
49
49
  this.tags = metadata.tags;
50
50
  this.webhook = metadata.webhook;
51
51
  this.form = metadata.form;
52
+ this.uiTrigger = metadata.uiTrigger;
52
53
  this.script = script;
53
54
  this.readme = readme;
54
55
  this.manifest = settings.dependencies.scopedToProcess && manifest !== null ? manifest : { dependencies: [] };
@@ -65,6 +66,9 @@ class Process extends Resource {
65
66
  tags: this.tags || [],
66
67
  webhook: this.webhook || null,
67
68
  form: this.form || null,
69
+ // Inserted only when set, so every process that predates UI triggers keeps the
70
+ // content hash already stored in .fcode (mirrors Team.contentHash).
71
+ ...(this.uiTrigger && { uiTrigger: this.uiTrigger }),
68
72
  manifestDependencies: this.manifest.dependencies,
69
73
  dependenciesScopedToProcess: this.settings.dependencies.scopedToProcess,
70
74
  });
@@ -185,12 +189,22 @@ exports.Variable = Variable;
185
189
  Variable.ENV_KEY_MAX_LENGTH = 64;
186
190
  Variable.ENV_KEY_REGEX = /^[A-Z_a-z]\w*$/;
187
191
  class ProgrammingLanguageManifest extends Resource {
188
- constructor(id, slug, programmingLanguage, dependencies) {
192
+ constructor(id, slug, programmingLanguage, dependencies, inheritedDependencies = []) {
189
193
  super();
190
194
  this.id = id;
191
195
  this.slug = slug;
192
196
  this.programmingLanguage = programmingLanguage;
193
197
  this.dependencies = dependencies;
198
+ this.inheritedDependencies = inheritedDependencies;
199
+ }
200
+ /**
201
+ * What an execution installs: this workspace's dependencies layered over the inherited ones, one
202
+ * entry per package name. Mirrors the platform's merge so a local run resolves the same install
203
+ * directory the cloud does.
204
+ */
205
+ get effectiveDependencies() {
206
+ const ownNames = new Set(this.dependencies.map((dependency) => dependency.name));
207
+ return [...this.dependencies, ...this.inheritedDependencies.filter((dependency) => !ownNames.has(dependency.name))];
194
208
  }
195
209
  get contentHash() {
196
210
  return crypto
@@ -214,7 +228,9 @@ class Team extends Resource {
214
228
  const payload = {
215
229
  zoneId: this.zoneId || null,
216
230
  errorHandlerConfig: this.errorHandlerConfig || null,
217
- parentTeamSlugs: [...(this.parentTeamSlugs || [])].sort(),
231
+ // Kept in the given order, not sorted: the order is the inheritance precedence, so a pure
232
+ // reorder is a real change that must be detected and pushed.
233
+ parentTeamSlugs: [...(this.parentTeamSlugs || [])],
218
234
  };
219
235
  // Inserted only when set, so every team.json that predates webhook auth keeps the
220
236
  // hash already stored in .fcode and does not report a phantom conflict. A key present
@@ -227,6 +243,18 @@ class Team extends Resource {
227
243
  if (this.primaryLocale) {
228
244
  payload.primaryLocale = this.primaryLocale;
229
245
  }
246
+ // Versions hash only their tags and aliases their name->tag pairs: createdAt is
247
+ // server-assigned and a comment is immutable once published (there is no update mutation),
248
+ // so neither can represent a pushable local change. Inserted only when non-empty, again to
249
+ // keep hashes stored before versioning existed valid.
250
+ if (this.versions && this.versions.length > 0) {
251
+ payload.versions = this.versions.map(({ tag }) => tag).sort();
252
+ }
253
+ if (this.aliases && this.aliases.length > 0) {
254
+ payload.aliases = this.aliases
255
+ .map(({ name, tag }) => ({ name, tag }))
256
+ .sort((a, b) => a.name.localeCompare(b.name));
257
+ }
230
258
  return (0, canonical_hash_1.canonicalJsonHash)(payload);
231
259
  }
232
260
  }
@@ -12,7 +12,10 @@ async function getDependenciesPath(programmingLanguage) {
12
12
  const basePath = (0, workspace_1.baseDependenciesPath)(programmingLanguage);
13
13
  const repo = new programming_language_manifest_1.default();
14
14
  const programmingLanguageManifest = await repo.find(programmingLanguage);
15
- const dependenciesDirectory = (0, env_dependencies_command_1.dependenciesHash)(programmingLanguageManifest.dependencies);
15
+ // The effective set, not the workspace's own: the cloud installs the inherited dependencies too,
16
+ // and the directory is named after the hash of what is installed. Hashing only the own list here
17
+ // would make a local run resolve a different directory than the platform.
18
+ const dependenciesDirectory = (0, env_dependencies_command_1.dependenciesHash)(programmingLanguageManifest.effectiveDependencies);
16
19
  return path.join(basePath, dependenciesDirectory);
17
20
  }
18
21
  function getProcessDependenciesPath(process) {
package/dist/env.js CHANGED
@@ -14,7 +14,7 @@ const zod_1 = require("zod");
14
14
  */
15
15
  exports.envSchema = zod_1.z.object({
16
16
  // --- Cloud / auth endpoints ---
17
- FCODE_CLOUD_URL: zod_1.z.string().min(1).default('https://code.factorial.dev/platform'),
17
+ FCODE_CLOUD_URL: zod_1.z.string().min(1).default('https://code.factorialhr.com/platform'),
18
18
  FCODE_AUTH_URL: zod_1.z.string().min(1).optional(),
19
19
  FCODE_CLIENT_ID: zod_1.z.string().min(1).default('cli'),
20
20
  // --- Local config location (defaults to os.homedir() at the call site) ---
@@ -136,6 +136,7 @@ class ProcessRepository extends resource_1.default {
136
136
  tags: Array.isArray(raw.tags) ? raw.tags : [],
137
137
  webhook: raw.webhook,
138
138
  form: raw.form,
139
+ uiTrigger: raw.uiTrigger,
139
140
  };
140
141
  }
141
142
  catch (error) {
@@ -156,6 +157,9 @@ class ProcessRepository extends resource_1.default {
156
157
  if (process.form !== undefined) {
157
158
  metadata.form = process.form;
158
159
  }
160
+ if (process.uiTrigger !== undefined) {
161
+ metadata.uiTrigger = process.uiTrigger;
162
+ }
159
163
  return metadata;
160
164
  }
161
165
  async findParameters(processSlug) {
@@ -1,14 +1,30 @@
1
- import { ResourceMetadata, ProgrammingLanguageManifest } from '../core/types';
1
+ import { ProgrammingLanguage, ProgrammingLanguageManifest, ResourceMetadata } from '../core/types';
2
2
  import ResourceRepositoryBase from './resource';
3
+ /**
4
+ * The inherited file each language collapses its parents into. Exported so the gitignore sync names
5
+ * exactly what this repository writes, rather than repeating the strings.
6
+ */
7
+ export declare const INHERITED_FILE_NAME: Record<ProgrammingLanguage, string>;
8
+ export declare const DEPENDENCIES_DIR = "dependencies";
3
9
  declare class ProgrammingLanguageManifestRepository extends ResourceRepositoryBase<ProgrammingLanguageManifest> {
4
10
  constructor();
5
11
  find(slug: string): Promise<ProgrammingLanguageManifest>;
6
12
  protected getSourceCodeFileName(programmingLanguageManifest: ProgrammingLanguageManifest): string;
13
+ /**
14
+ * The inherited dependencies of one language, collapsed into a single file the way inherited
15
+ * variables are (`variables.inherited.env`) rather than one file per parent: two versions of the
16
+ * same package cannot be installed side by side, so there is only ever one winner per name and
17
+ * the platform has already picked it.
18
+ */
19
+ protected getInheritedFileName(programmingLanguage: ProgrammingLanguage): string;
20
+ getInheritedPath(programmingLanguage: ProgrammingLanguage): string;
21
+ private readInheritedDependencies;
7
22
  protected getResourceDirectory(programmingLanguageManifest: ProgrammingLanguageManifest): string;
8
23
  protected getResourcePath(programmingLanguageManifest: ProgrammingLanguageManifest): string;
9
24
  protected removeFiles(programmingLanguageManifest: ProgrammingLanguageManifest): Promise<void>;
10
25
  protected resourceToMetadata(programmingLanguageManifest: ProgrammingLanguageManifest): ResourceMetadata;
11
26
  protected saveFiles(programmingLanguageManifest: ProgrammingLanguageManifest): Promise<void>;
27
+ private removeInheritedFile;
12
28
  metadataFromFiles(slug: string): Promise<ResourceMetadata>;
13
29
  validSlug(slug: string): boolean;
14
30
  slugFormatMessage(): string;
@@ -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):
@@ -8,6 +8,7 @@ declare const AuthService: {
8
8
  isAuthenticated(url: string): Promise<boolean>;
9
9
  isTokenExpiring(token: Token): boolean;
10
10
  refreshToken(authUrl: string, refreshToken: string): Promise<Token>;
11
+ refreshSession(url: string): Promise<boolean>;
11
12
  tryRefresh(url: string, { authUrl, token, userInfo }: AuthInfo): Promise<boolean>;
12
13
  login({ url, authUrl, email, password, clientId, clientSecret, accessToken }: Credentials): Promise<AuthInfo>;
13
14
  logout(url: string): Promise<void>;
@@ -117,6 +117,18 @@ const AuthService = {
117
117
  }
118
118
  throw token.error_description;
119
119
  },
120
+ // Force a re-mint of the access token regardless of expiry. Team groups are
121
+ // snapshotted into the token at issuance, so a token created before a team
122
+ // existed (or before the user joined it) 403s on it until re-minted (FCT-61231).
123
+ async refreshSession(url) {
124
+ try {
125
+ const authInfo = await this.globalConfig.getAuthInfo(url);
126
+ return await this.tryRefresh(url, authInfo);
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ },
120
132
  async tryRefresh(url, { authUrl, token, userInfo }) {
121
133
  if (!token.refresh_token) {
122
134
  return false;
@@ -10,6 +10,7 @@ declare class FcodeGraphQLClient {
10
10
  private teamSlug;
11
11
  private url;
12
12
  constructor(options: FcodeGraphQLClientOptions);
13
+ setAccessToken(accessToken: string): void;
13
14
  isDuplicatedError(error: any): boolean;
14
15
  isEmptyValueError(error: any): boolean;
15
16
  cleanError(error: any): any;
@@ -63,9 +64,11 @@ declare class FcodeGraphQLClient {
63
64
  /** The API upserts: an existing alias with the same name is re-pointed to the tag. */
64
65
  createTeamVersionAlias(name: string, tag: string): Promise<TeamVersionAlias>;
65
66
  deleteTeamVersionAlias(id: string): Promise<TeamVersionAlias>;
67
+ verifyTeamAccess(): Promise<void>;
66
68
  team(): Promise<RawTeamSettings>;
67
69
  pushTeam(update: TeamUpdateInput): Promise<RawTeamSettings>;
68
70
  mapProgrammingLanguageManifest(programmingLanguageManifest: any): ProgrammingLanguageManifest;
71
+ private mapDependencies;
69
72
  allProgrammingLanguageManifest(): Promise<Array<ProgrammingLanguageManifest>>;
70
73
  pushProgrammingLanguageManifest(programmingLanguageManifest: ProgrammingLanguageManifest): Promise<ProgrammingLanguageManifest>;
71
74
  allServiceAccounts(): Promise<Array<ServiceAccount>>;