@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
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/2.2.0 linux-x64 node-v22.22.3
23
+ @factorialco/fcode-cli/2.4.0 linux-x64 node-v22.22.3
24
24
  $ fcode --help [COMMAND]
25
25
  USAGE
26
26
  $ fcode COMMAND
@@ -998,14 +998,17 @@ EXAMPLES
998
998
 
999
999
  ## `fcode team:pull`
1000
1000
 
1001
- Fetch team settings (timezone, error handler, inheritance) from cloud into team.json
1001
+ Fetch team settings (timezone, error handler, inheritance) and team versions/aliases from cloud into team.json
1002
1002
 
1003
1003
  ```
1004
1004
  USAGE
1005
- $ fcode team:pull
1005
+ $ fcode team:pull [-f]
1006
+
1007
+ FLAGS
1008
+ -f, --force Force command
1006
1009
 
1007
1010
  DESCRIPTION
1008
- Fetch team settings (timezone, error handler, inheritance) from cloud into team.json
1011
+ Fetch team settings (timezone, error handler, inheritance) and team versions/aliases from cloud into team.json
1009
1012
 
1010
1013
  EXAMPLES
1011
1014
  $ fcode team:pull
@@ -1013,7 +1016,7 @@ EXAMPLES
1013
1016
 
1014
1017
  ## `fcode team:push`
1015
1018
 
1016
- Update cloud team settings (timezone, error handler, inheritance, webhook auth) from team.json
1019
+ Update cloud team settings (timezone, error handler, inheritance, webhook auth) and sync team versions/aliases from team.json
1017
1020
 
1018
1021
  ```
1019
1022
  USAGE
@@ -1023,7 +1026,8 @@ FLAGS
1023
1026
  -f, --force Force command
1024
1027
 
1025
1028
  DESCRIPTION
1026
- Update cloud team settings (timezone, error handler, inheritance, webhook auth) from team.json
1029
+ Update cloud team settings (timezone, error handler, inheritance, webhook auth) and sync team versions/aliases from
1030
+ team.json
1027
1031
 
1028
1032
  EXAMPLES
1029
1033
  $ fcode team:push
@@ -1031,14 +1035,14 @@ EXAMPLES
1031
1035
 
1032
1036
  ## `fcode team:status`
1033
1037
 
1034
- Show whether team settings (timezone, error handler, inheritance) differ from the cloud
1038
+ Show whether team settings (timezone, error handler, inheritance) or team versions/aliases differ from the cloud
1035
1039
 
1036
1040
  ```
1037
1041
  USAGE
1038
1042
  $ fcode team:status
1039
1043
 
1040
1044
  DESCRIPTION
1041
- Show whether team settings (timezone, error handler, inheritance) differ from the cloud
1045
+ Show whether team settings (timezone, error handler, inheritance) or team versions/aliases differ from the cloud
1042
1046
 
1043
1047
  EXAMPLES
1044
1048
  $ fcode team:status
@@ -7,6 +7,7 @@ const path = require("node:path");
7
7
  const global_1 = require("../config/global");
8
8
  const workspace_1 = require("../config/workspace");
9
9
  const pull_1 = require("./pull");
10
+ const team_access_1 = require("../services/team-access");
10
11
  const types_1 = require("../core/types");
11
12
  const cli_1 = require("../utils/cli");
12
13
  const settings_1 = require("../config/settings");
@@ -120,6 +121,7 @@ class Clone extends core_1.Command {
120
121
  });
121
122
  }
122
123
  async catch(error) {
124
+ process.exitCode = 1;
123
125
  const suggestions = error?.suggestions ?? [];
124
126
  const hint = suggestions.length > 0 ? ` Try this: ${suggestions.map((suggestion) => `'${suggestion}'`).join('\n')} \n` : '';
125
127
  cli_1.default.error(error.message + hint);
@@ -180,8 +182,19 @@ class Clone extends core_1.Command {
180
182
  if (skipSkillsSetup === undefined) {
181
183
  skipSkillsSetup = (await new global_1.default().getSetting(settings_1.SettingKeys.SKIP_SKILLS_SETUP)) === 'true';
182
184
  }
185
+ // Pre-flight access check so a session without access to the team fails here,
186
+ // before any workspace directory, config, or skills are created.
187
+ await team_access_1.default.ensureTeamAccess(url, slug);
183
188
  await this.cloneTeam({ url, slug }, skipSkillsSetup);
184
- cli_1.default.log(`\n🎉 ${args.team} team workspace cloned successfully!\n`);
189
+ // Failed pulls don't abort the clone: each sub-pull reports through
190
+ // BaseCommand.catch, which prints and sets a non-zero exit code.
191
+ if (process.exitCode) {
192
+ cli_1.default.log(`\n⚠️ '${args.team}' team workspace cloned with errors — some resources failed to pull.`);
193
+ cli_1.default.log(` Fix the errors above, then run 'fcode pull' inside the workspace.\n`);
194
+ }
195
+ else {
196
+ cli_1.default.log(`\n🎉 ${args.team} team workspace cloned successfully!\n`);
197
+ }
185
198
  cli_1.default.log(`$ cd ${slug}\n`);
186
199
  }
187
200
  }
@@ -1,9 +1,20 @@
1
+ import { table } from '../../utils/cli/styled/table';
1
2
  import DependencyCommand from '../../core/commands/dependency-command';
3
+ import { type MergedResource } from '../../core/types';
4
+ export declare const inheritedCount: (resource: MergedResource) => number;
5
+ /**
6
+ * A manifest is never itself inherited — every workspace owns one per language — so the column
7
+ * counts the entries its parents contribute rather than naming one workspace the way the other
8
+ * `status` commands do. Empty when nothing is inherited, so the column is dropped entirely, like
9
+ * theirs.
10
+ */
11
+ export declare const inheritedColumn: (resources: Array<MergedResource>) => table.Columns<MergedResource>;
2
12
  export default class DependenciesStatus extends DependencyCommand {
3
13
  static description: string;
4
14
  static examples: string[];
5
15
  static args: {
6
16
  resourceSlug: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
17
  };
18
+ resourcesTableLog(resources: Array<MergedResource>): Promise<void>;
8
19
  run(): Promise<void>;
9
20
  }
@@ -1,8 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inheritedColumn = exports.inheritedCount = void 0;
4
+ const cli_1 = require("../../utils/cli");
3
5
  const dependency_command_1 = require("../../core/commands/dependency-command");
4
6
  const types_1 = require("../../core/types");
7
+ const colorize_resource_1 = require("../../utils/colorize-resource");
8
+ const inheritedCount = (resource) => resource.inheritedDependencies?.length ?? 0;
9
+ exports.inheritedCount = inheritedCount;
10
+ /**
11
+ * A manifest is never itself inherited — every workspace owns one per language — so the column
12
+ * counts the entries its parents contribute rather than naming one workspace the way the other
13
+ * `status` commands do. Empty when nothing is inherited, so the column is dropped entirely, like
14
+ * theirs.
15
+ */
16
+ const inheritedColumn = (resources) => {
17
+ if (!resources.some((resource) => (0, exports.inheritedCount)(resource) > 0)) {
18
+ return {};
19
+ }
20
+ return {
21
+ inherited: {
22
+ get(resource) {
23
+ const count = (0, exports.inheritedCount)(resource);
24
+ return (0, colorize_resource_1.colorizeByStatus)(count > 0 ? `🔗 ${count}` : '', resource.status);
25
+ },
26
+ header: 'inherited',
27
+ },
28
+ };
29
+ };
30
+ exports.inheritedColumn = inheritedColumn;
5
31
  class DependenciesStatus extends dependency_command_1.default {
32
+ async resourcesTableLog(resources) {
33
+ cli_1.cli.table(resources, {
34
+ name: {
35
+ get({ slug, status }) {
36
+ return (0, colorize_resource_1.colorizeByStatus)(slug, status);
37
+ },
38
+ header: 'name',
39
+ },
40
+ ...(0, exports.inheritedColumn)(resources),
41
+ status: {
42
+ get({ status }) {
43
+ return (0, colorize_resource_1.colorizeByStatus)(status, status);
44
+ },
45
+ header: 'status',
46
+ },
47
+ }, { sort: 'name' });
48
+ }
6
49
  async run() {
7
50
  await this.resourcesLog();
8
51
  }
@@ -23,7 +23,7 @@ class PullCommand extends base_command_1.default {
23
23
  await pull_5.default.run(args);
24
24
  // Team settings are pulled last so process metadata is available to resolve the
25
25
  // error-handler process id to its slug.
26
- await pull_6.default.run([]);
26
+ await pull_6.default.run(args);
27
27
  // Keep inherited resources out of git now that all resource metadata is saved.
28
28
  await (0, gitignore_1.syncInheritedGitignore)();
29
29
  }
@@ -3,6 +3,9 @@ import TeamRepository from '../../repositories/team';
3
3
  export default class TeamPullCommand extends ApiClientCommand {
4
4
  static description: string;
5
5
  static examples: string[];
6
+ static flags: {
7
+ force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
+ };
6
9
  _teamRepository: TeamRepository;
7
10
  run(): Promise<void>;
8
11
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const color_1 = require("@oclif/color");
4
4
  const api_client_command_1 = require("../../core/commands/api-client-command");
5
5
  const team_1 = require("../../repositories/team");
6
+ const types_1 = require("../../core/types");
6
7
  const cli_1 = require("../../utils/cli");
7
8
  class TeamPullCommand extends api_client_command_1.default {
8
9
  constructor() {
@@ -10,8 +11,19 @@ class TeamPullCommand extends api_client_command_1.default {
10
11
  this._teamRepository = new team_1.default();
11
12
  }
12
13
  async run() {
13
- await this.parse(TeamPullCommand);
14
+ const { flags: { force }, } = await this.parse(TeamPullCommand);
14
15
  cli_1.default.log(color_1.default.cyan('\n🗄️ pulling team settings...'));
16
+ // Local edits (a version tag added, an alias re-pointed) would be silently wiped by the
17
+ // overwrite below, so an unpushed team.json blocks the pull. No stored hash means there is
18
+ // no baseline to have diverged from (fresh workspace), so the pull proceeds.
19
+ if (!force && this._teamRepository.exists()) {
20
+ const local = await this._teamRepository.find();
21
+ const storedHash = await this.workspaceConfig.getTeamStoredHash();
22
+ if (storedHash !== undefined && storedHash !== local.contentHash) {
23
+ cli_1.default.log(color_1.default.yellow('🔺 team.json has unpushed local changes — run fcode team:push first, or fcode team:pull --force to overwrite them'));
24
+ return;
25
+ }
26
+ }
15
27
  const [raw, versions, aliases] = await Promise.all([
16
28
  this._fcodeGraphQLClient.team(),
17
29
  this._fcodeGraphQLClient.allTeamVersions(),
@@ -20,11 +32,10 @@ class TeamPullCommand extends api_client_command_1.default {
20
32
  const team = await this._teamRepository.fromRaw(raw);
21
33
  this._teamRepository.applyCloudVersioning(team, versions, aliases);
22
34
  this._teamRepository.save(team);
23
- // The stored hash intentionally excludes versions/aliases (see Team.contentHash), so
24
- // pull-only fields never surface as a phantom MODIFIED state.
25
35
  await this.workspaceConfig.setTeamStoredHash(team.contentHash);
26
36
  }
27
37
  }
28
- TeamPullCommand.description = 'Fetch team settings (timezone, error handler, inheritance) from cloud into team.json';
38
+ TeamPullCommand.description = 'Fetch team settings (timezone, error handler, inheritance) and team versions/aliases from cloud into team.json';
29
39
  TeamPullCommand.examples = ['$ fcode team:pull'];
40
+ TeamPullCommand.flags = types_1.ForceFlag;
30
41
  exports.default = TeamPullCommand;
@@ -8,4 +8,6 @@ export default class TeamPushCommand extends ApiClientCommand {
8
8
  };
9
9
  _teamRepository: TeamRepository;
10
10
  run(): Promise<void>;
11
+ /** Makes the cloud's team versions/aliases match team.json; returns failed manifest entries. */
12
+ private syncVersioning;
11
13
  }
@@ -4,6 +4,7 @@ const color_1 = require("@oclif/color");
4
4
  const api_client_command_1 = require("../../core/commands/api-client-command");
5
5
  const team_1 = require("../../repositories/team");
6
6
  const types_1 = require("../../core/types");
7
+ const team_versioning_1 = require("../../services/team-versioning");
7
8
  const cli_1 = require("../../utils/cli");
8
9
  class TeamPushCommand extends api_client_command_1.default {
9
10
  constructor() {
@@ -22,9 +23,6 @@ class TeamPushCommand extends api_client_command_1.default {
22
23
  cli_1.default.log('Team settings already up to date');
23
24
  return;
24
25
  }
25
- // team.json's `versions`/`aliases` fields are pull-only and intentionally left out of the
26
- // update payload: team versions and aliases are only written through the explicit
27
- // team:versions/team:aliases commands.
28
26
  const update = { parentTeamSlugs: team.parentTeamSlugs };
29
27
  if (team.zoneId !== undefined) {
30
28
  update.zoneId = team.zoneId;
@@ -42,11 +40,64 @@ class TeamPushCommand extends api_client_command_1.default {
42
40
  // so removing primaryLocale from it must clear the cloud setting. Empty string is the clear.
43
41
  update.primaryLocale = team.primaryLocale ?? '';
44
42
  await this._fcodeGraphQLClient.pushTeam(update);
43
+ const failedEntities = await this.syncVersioning(team, force);
44
+ // Re-read the cloud state so server-assigned fields (createdAt) and declined deletions
45
+ // land back in team.json, then hash exactly what was written.
46
+ const [versions, aliases] = await Promise.all([
47
+ this._fcodeGraphQLClient.allTeamVersions(),
48
+ this._fcodeGraphQLClient.allTeamVersionAliases(),
49
+ ]);
50
+ this._teamRepository.applyCloudVersioning(team, versions, aliases);
51
+ this._teamRepository.save(team);
45
52
  await this.workspaceConfig.setTeamStoredHash(team.contentHash);
46
53
  cli_1.default.log(color_1.default.green('✅ team settings pushed'));
54
+ // Release pipelines must be able to detect an incomplete version. Re-running the
55
+ // command converges: it publishes only what is still missing.
56
+ if (failedEntities > 0) {
57
+ this.error(`team versions pushed with ${failedEntities} failed entities`, { exit: 1 });
58
+ }
59
+ }
60
+ /** Makes the cloud's team versions/aliases match team.json; returns failed manifest entries. */
61
+ async syncVersioning(team, force) {
62
+ const [cloudVersions, cloudAliases] = await Promise.all([
63
+ this._fcodeGraphQLClient.allTeamVersions(),
64
+ this._fcodeGraphQLClient.allTeamVersionAliases(),
65
+ ]);
66
+ const plan = (0, team_versioning_1.planTeamVersioningSync)(team, cloudVersions, cloudAliases);
67
+ if ((0, team_versioning_1.teamVersioningSyncPlanIsEmpty)(plan)) {
68
+ return 0;
69
+ }
70
+ let failedEntities = 0;
71
+ for (const version of plan.createVersions) {
72
+ cli_1.default.log(color_1.default.cyan(`🏷️ publishing team version ${version.tag}...`));
73
+ const teamVersion = await this._fcodeGraphQLClient.createTeamVersion(version.tag, version.comment);
74
+ failedEntities += (0, team_versioning_1.logTeamVersionManifest)(teamVersion);
75
+ }
76
+ for (const version of plan.deleteVersions) {
77
+ if (!force) {
78
+ const confirmed = await cli_1.default.confirm(`Team version ${version.tag} exists in the cloud but not in team.json. Deleting it also deletes ` +
79
+ 'every process/module version with that tag, plus the aliases, executions and schedules ' +
80
+ 'referencing them. Delete it?');
81
+ if (!confirmed) {
82
+ continue;
83
+ }
84
+ }
85
+ cli_1.default.log(color_1.default.cyan(`🗑️ deleting team version ${version.tag}...`));
86
+ await this._fcodeGraphQLClient.deleteTeamVersion(version.tag);
87
+ }
88
+ for (const alias of plan.setAliases) {
89
+ cli_1.default.log(color_1.default.cyan(`🔗 pointing alias ${alias.name} at ${alias.tag}...`));
90
+ await this._fcodeGraphQLClient.createTeamVersionAlias(alias.name, alias.tag);
91
+ }
92
+ for (const alias of plan.deleteAliases) {
93
+ cli_1.default.log(color_1.default.cyan(`🗑️ deleting alias ${alias.name}...`));
94
+ await this._fcodeGraphQLClient.deleteTeamVersionAlias(alias.id);
95
+ }
96
+ return failedEntities;
47
97
  }
48
98
  }
49
- TeamPushCommand.description = 'Update cloud team settings (timezone, error handler, inheritance, webhook auth) from team.json';
99
+ TeamPushCommand.description = 'Update cloud team settings (timezone, error handler, inheritance, webhook auth) and sync ' +
100
+ 'team versions/aliases from team.json';
50
101
  TeamPushCommand.examples = ['$ fcode team:push'];
51
102
  TeamPushCommand.flags = types_1.ForceFlag;
52
103
  exports.default = TeamPushCommand;
@@ -18,7 +18,13 @@ class TeamStatusCommand extends api_client_command_1.default {
18
18
  }
19
19
  const local = await this._teamRepository.find();
20
20
  const storedHash = await this.workspaceConfig.getTeamStoredHash();
21
- const remote = await this._teamRepository.fromRaw(await this._fcodeGraphQLClient.team());
21
+ const [raw, versions, aliases] = await Promise.all([
22
+ this._fcodeGraphQLClient.team(),
23
+ this._fcodeGraphQLClient.allTeamVersions(),
24
+ this._fcodeGraphQLClient.allTeamVersionAliases(),
25
+ ]);
26
+ const remote = await this._teamRepository.fromRaw(raw);
27
+ this._teamRepository.applyCloudVersioning(remote, versions, aliases);
22
28
  const localChanged = storedHash !== local.contentHash;
23
29
  const remoteChanged = storedHash !== remote.contentHash;
24
30
  if (localChanged && remoteChanged) {
@@ -35,6 +41,6 @@ class TeamStatusCommand extends api_client_command_1.default {
35
41
  }
36
42
  }
37
43
  }
38
- TeamStatusCommand.description = 'Show whether team settings (timezone, error handler, inheritance) differ from the cloud';
44
+ TeamStatusCommand.description = 'Show whether team settings (timezone, error handler, inheritance) or team versions/aliases differ from the cloud';
39
45
  TeamStatusCommand.examples = ['$ fcode team:status'];
40
46
  exports.default = TeamStatusCommand;
@@ -9,5 +9,4 @@ export default class TeamVersionsCreateCommand extends ApiClientCommand {
9
9
  comment: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
10
  };
11
11
  run(): Promise<void>;
12
- private logManifest;
13
12
  }
@@ -4,13 +4,14 @@ const core_1 = require("@oclif/core");
4
4
  const color_1 = require("@oclif/color");
5
5
  const api_client_command_1 = require("../../../core/commands/api-client-command");
6
6
  const pull_1 = require("../../pull");
7
+ const team_versioning_1 = require("../../../services/team-versioning");
7
8
  const cli_1 = require("../../../utils/cli");
8
9
  class TeamVersionsCreateCommand extends api_client_command_1.default {
9
10
  async run() {
10
11
  const { args: { tag }, flags: { comment }, } = await this.parse(TeamVersionsCreateCommand);
11
12
  cli_1.default.log(color_1.default.cyan(`\n🏷️ publishing team version ${tag}...`));
12
13
  const teamVersion = await this._fcodeGraphQLClient.createTeamVersion(tag, comment);
13
- const failedCount = this.logManifest(teamVersion);
14
+ const failedCount = (0, team_versioning_1.logTeamVersionManifest)(teamVersion);
14
15
  // Refresh the local workspace: per-entity versions/<tag>/ folders and team.json.
15
16
  await pull_1.default.run([]);
16
17
  // Release pipelines must be able to detect an incomplete version. Re-running the
@@ -19,16 +20,6 @@ class TeamVersionsCreateCommand extends api_client_command_1.default {
19
20
  this.error(`team version ${tag} published with ${failedCount} failed entities`, { exit: 1 });
20
21
  }
21
22
  }
22
- logManifest(teamVersion) {
23
- const created = teamVersion.manifest.filter(({ outcome }) => outcome === 'CREATED');
24
- const skipped = teamVersion.manifest.filter(({ outcome }) => outcome === 'SKIPPED_EXISTS');
25
- const failed = teamVersion.manifest.filter(({ outcome }) => outcome === 'FAILED');
26
- cli_1.default.log(color_1.default.green(`✅ ${created.length} created`) + `, ${skipped.length} skipped (tag already exists)` + (failed.length > 0 ? `, ${color_1.default.red(`${failed.length} failed`)}` : ''));
27
- for (const entry of failed) {
28
- cli_1.default.error(`${entry.resourceType.toLowerCase()} ${entry.slug} — ${entry.error ?? 'unknown error'}`);
29
- }
30
- return failed.length;
31
- }
32
23
  }
33
24
  TeamVersionsCreateCommand.description = 'Publish a team version: creates a version with the tag on every owned process and module ' +
34
25
  '(existing tags are skipped) and pins bare module imports in the published snapshots';
@@ -3,4 +3,5 @@ import FcodeGraphQLClient from '../../services/fcode-graphql-client';
3
3
  export default abstract class ApiClientCommand extends BaseCommand {
4
4
  _fcodeGraphQLClient: FcodeGraphQLClient;
5
5
  init(): Promise<void>;
6
+ protected onSessionRefreshed(): Promise<void>;
6
7
  }
@@ -26,5 +26,10 @@ class ApiClientCommand extends base_command_1.default {
26
26
  throw error;
27
27
  }
28
28
  }
29
+ async onSessionRefreshed() {
30
+ const { url } = await this.workspaceConfig.getTeamOrigin();
31
+ const { token: { access_token: accessToken }, } = await this.globalConfig.getAuthInfo(url);
32
+ this._fcodeGraphQLClient.setAccessToken(accessToken);
33
+ }
29
34
  }
30
35
  exports.default = ApiClientCommand;
@@ -5,7 +5,9 @@ export default abstract class BaseCommand extends Command {
5
5
  url?: string;
6
6
  globalConfig: GlobalConfig;
7
7
  workspaceConfig: WorkspaceConfig;
8
+ private sessionRefreshRetried;
8
9
  catch(error: any): Promise<any>;
9
10
  init(url?: string): Promise<void>;
11
+ protected onSessionRefreshed(): Promise<void>;
10
12
  protected logActiveTeam(): Promise<void>;
11
13
  }
@@ -4,16 +4,42 @@ const core_1 = require("@oclif/core");
4
4
  const global_1 = require("../../config/global");
5
5
  const workspace_1 = require("../../config/workspace");
6
6
  const color_1 = require("@oclif/color");
7
+ const auth_service_1 = require("../../services/auth-service");
7
8
  const errors_1 = require("../errors");
8
9
  const cli_1 = require("../../utils/cli");
9
10
  const url_utils_1 = require("../../utils/url-utils");
10
11
  class BaseCommand extends core_1.Command {
12
+ constructor() {
13
+ super(...arguments);
14
+ this.sessionRefreshRetried = false;
15
+ }
11
16
  async catch(error) {
12
17
  // do any extra work with error
13
18
  if (error && error.response && error.response.status === 401) {
19
+ process.exitCode = 1;
14
20
  cli_1.default.error(`User authentication is invalid for current team.`);
15
21
  return;
16
22
  }
23
+ if (error && error.response && error.response.status === 403) {
24
+ let origin;
25
+ try {
26
+ origin = await this.workspaceConfig.getTeamOrigin();
27
+ }
28
+ catch {
29
+ // No workspace context; report without the slug.
30
+ }
31
+ // A 403 with a valid session usually means the token's team groups went
32
+ // stale — they are snapshotted at issuance (FCT-61231). Re-mint the token
33
+ // and retry the command once; a second denial falls through to the error.
34
+ if (!this.sessionRefreshRetried && origin && (await auth_service_1.default.refreshSession(origin.url))) {
35
+ this.sessionRefreshRetried = true;
36
+ await this.onSessionRefreshed();
37
+ return this.run().catch((retryError) => this.catch(retryError));
38
+ }
39
+ process.exitCode = 1;
40
+ cli_1.default.error((0, errors_1.teamAccessDeniedMessage)(origin?.slug));
41
+ return;
42
+ }
17
43
  if (error instanceof errors_1.NotAuthenticated) {
18
44
  let errorMessage = 'No session active. Logging you in';
19
45
  const loginArgs = [];
@@ -40,9 +66,11 @@ class BaseCommand extends core_1.Command {
40
66
  return this.run();
41
67
  }
42
68
  if (error instanceof errors_1.CLIError) {
69
+ process.exitCode = 1;
43
70
  cli_1.default.error(error);
44
71
  return;
45
72
  }
73
+ process.exitCode = 1;
46
74
  cli_1.default.error(error.message);
47
75
  }
48
76
  async init(url) {
@@ -53,6 +81,9 @@ class BaseCommand extends core_1.Command {
53
81
  const { authUrl } = await this.globalConfig.getAuthInfo(workspaceUrl);
54
82
  await this.config.runHook('auth', { argv: [workspaceUrl, authUrl], id: 'init' });
55
83
  }
84
+ // Called after a 403-triggered session refresh, before the retry: rewire
85
+ // anything that captured the old access token. API commands override this.
86
+ async onSessionRefreshed() { }
56
87
  async logActiveTeam() {
57
88
  const { slug, url } = await this.workspaceConfig.getTeamOrigin();
58
89
  cli_1.default.log(color_1.default.magenta(`💻 Working on Factorial Code team '${slug}${url !== global_1.FCODE_CLOUD_URL ? ` (${url})` : ''}'.`));
@@ -25,7 +25,10 @@ class EnvDependenciesCommand extends api_client_command_1.default {
25
25
  fs.rmSync(this.envPath, { recursive: true });
26
26
  }
27
27
  const manifest = await this.getManifest();
28
- const installationPath = this.installationPath(manifest.dependencies);
28
+ // The effective set — own dependencies over inherited ones — because that is what the platform
29
+ // installs, and the directory is named after the hash of what is installed.
30
+ const dependencies = manifest.effectiveDependencies;
31
+ const installationPath = this.installationPath(dependencies);
29
32
  if (fs.existsSync(installationPath)) {
30
33
  if (force) {
31
34
  fs.rmSync(installationPath, { recursive: true });
@@ -38,12 +41,12 @@ class EnvDependenciesCommand extends api_client_command_1.default {
38
41
  await this.createEnv();
39
42
  cli_1.default.log(`Installing Factorial Code ${(0, types_1.programmingLanguageDisplayName)(this.programmingLanguage)} dependencies... (This process might take some time ⌛️)`);
40
43
  cli_1.default.log('====================================');
41
- await this.installDependencies(installationPath, manifest.dependencies);
44
+ await this.installDependencies(installationPath, dependencies);
42
45
  cli_1.default.log('====================================');
43
46
  cli_1.default.log(color_1.default.green(`Factorial Code ${(0, types_1.programmingLanguageDisplayName)(this.programmingLanguage)} dependencies successfully installed in ${installationPath} 🤩\n\n`));
44
47
  }
45
48
  async check() {
46
- const installationPath = this.installationPath((await this.getManifest()).dependencies);
49
+ const installationPath = this.installationPath((await this.getManifest()).effectiveDependencies);
47
50
  if (!fs.existsSync(installationPath)) {
48
51
  cli_1.default.log(color_1.default.yellow(`⚠️ ${(0, types_1.programmingLanguageDisplayName)(this.programmingLanguage)} dependencies need to be installed.`));
49
52
  }
@@ -47,6 +47,10 @@ export declare class NotAuthenticated extends CLIError {
47
47
  constructor(url: string);
48
48
  getUrl(): string;
49
49
  }
50
+ export declare function teamAccessDeniedMessage(slug?: string): string;
51
+ export declare class TeamAccessDenied extends CLIError {
52
+ constructor(slug?: string);
53
+ }
50
54
  export declare class NotFcodeWorkspace extends CLIError {
51
55
  constructor();
52
56
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ErrorExecutingProcess = exports.ProcessDependenciesNotInstalled = exports.DependenciesNotInstalled = exports.NotFcodeWorkspace = exports.NotAuthenticated = exports.CloudProgrammingLanguageManifestNotFound = exports.LocalProgrammingLanguageManifestNotFound = exports.CloudLocaleNotFound = exports.LocalLocaleNotFound = exports.CloudVariableNotFound = exports.LocalVariableNotFound = exports.CloudScriptLibraryNotFound = exports.LocalScriptLibraryNotFound = exports.CloudProcessNotFound = exports.LocalProcessNotFound = exports.ResourceNotFound = exports.CLIError = void 0;
3
+ exports.ErrorExecutingProcess = exports.ProcessDependenciesNotInstalled = exports.DependenciesNotInstalled = exports.NotFcodeWorkspace = exports.TeamAccessDenied = exports.NotAuthenticated = exports.CloudProgrammingLanguageManifestNotFound = exports.LocalProgrammingLanguageManifestNotFound = exports.CloudLocaleNotFound = exports.LocalLocaleNotFound = exports.CloudVariableNotFound = exports.LocalVariableNotFound = exports.CloudScriptLibraryNotFound = exports.LocalScriptLibraryNotFound = exports.CloudProcessNotFound = exports.LocalProcessNotFound = exports.ResourceNotFound = exports.CLIError = void 0;
4
+ exports.teamAccessDeniedMessage = teamAccessDeniedMessage;
4
5
  const global_1 = require("../config/global");
5
6
  class CLIError extends Error {
6
7
  constructor(message, options) {
@@ -120,6 +121,20 @@ class NotAuthenticated extends CLIError {
120
121
  }
121
122
  }
122
123
  exports.NotAuthenticated = NotAuthenticated;
124
+ function teamAccessDeniedMessage(slug) {
125
+ const team = slug ? ` '${slug}'` : '';
126
+ return (`You don't have access to team${team} with your current session.` +
127
+ ` If you were granted access recently, run 'fcode logout' and 'fcode login' and retry.`);
128
+ }
129
+ class TeamAccessDenied extends CLIError {
130
+ constructor(slug) {
131
+ super(teamAccessDeniedMessage(slug), {
132
+ code: 'ERR_TEAM_ACCESS_DENIED',
133
+ exit: 1,
134
+ });
135
+ }
136
+ }
137
+ exports.TeamAccessDenied = TeamAccessDenied;
123
138
  class NotFcodeWorkspace extends CLIError {
124
139
  constructor() {
125
140
  super('⚠️ Current directory is not a Factorial Code workspace.', {
@@ -593,13 +593,22 @@ const main = typeof _hooksModule['${fn}'] === 'function'
593
593
  }
594
594
  };
595
595
  })(),
596
- // The third argument is the version options the cloud honours. A workspace directory holds
597
- // no published snapshots, so a pinned call resolves against the files on disk — the same
598
- // per-file fallback the cloud applies to a locale that was never published at that tag.
596
+ // A workspace directory holds no published snapshots, so a pinned call resolves against
597
+ // the files on disk — the same per-file fallback the cloud applies to a locale that was
598
+ // never published at that tag. A locale option resolves against that locale's own files,
599
+ // falling back key by key to the merged translations, exactly like the cloud executors.
599
600
  // Own properties only and no array arguments, exactly like the cloud executors.
600
- i18n: Object.assign((key, args, _options) => {
601
+ i18n: Object.assign((key, args, options) => {
601
602
  const translations = ${JSON.stringify(metadata.i18n.translations, null, 2)};
602
- const template = Object.hasOwn(translations, key) ? translations[key] : undefined;
603
+ const byLocale = ${JSON.stringify(metadata.i18n.byLocale, null, 2)};
604
+ const requested = options && typeof options === 'object' && options.locale !== undefined && options.locale !== null
605
+ ? String(options.locale)
606
+ : undefined;
607
+ const localeMap = requested !== undefined && Object.hasOwn(byLocale, requested) ? byLocale[requested] : undefined;
608
+ let template = localeMap && Object.hasOwn(localeMap, key) ? localeMap[key] : undefined;
609
+ if (template === undefined) {
610
+ template = Object.hasOwn(translations, key) ? translations[key] : undefined;
611
+ }
603
612
  if (template === undefined) {
604
613
  return key;
605
614
  }