@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
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.3.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
@@ -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';
@@ -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
  }
@@ -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
  }
@@ -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
@@ -229,6 +243,18 @@ class Team extends Resource {
229
243
  if (this.primaryLocale) {
230
244
  payload.primaryLocale = this.primaryLocale;
231
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
+ }
232
258
  return (0, canonical_hash_1.canonicalJsonHash)(payload);
233
259
  }
234
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;