@factorialco/fcode-cli 1.5.0 → 2.1.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 (43) hide show
  1. package/README.md +123 -3
  2. package/dist/commands/http.d.ts +6 -4
  3. package/dist/commands/http.js +40 -24
  4. package/dist/commands/setup-debug.js +2 -2
  5. package/dist/commands/team/aliases/delete.d.ts +9 -0
  6. package/dist/commands/team/aliases/delete.js +28 -0
  7. package/dist/commands/team/aliases/list.d.ts +6 -0
  8. package/dist/commands/team/aliases/list.js +23 -0
  9. package/dist/commands/team/aliases/set.d.ts +10 -0
  10. package/dist/commands/team/aliases/set.js +25 -0
  11. package/dist/commands/team/pull.js +8 -1
  12. package/dist/commands/team/push.js +9 -1
  13. package/dist/commands/team/versions/create.d.ts +13 -0
  14. package/dist/commands/team/versions/create.js +42 -0
  15. package/dist/commands/team/versions/delete.d.ts +12 -0
  16. package/dist/commands/team/versions/delete.js +32 -0
  17. package/dist/commands/team/versions/list.d.ts +6 -0
  18. package/dist/commands/team/versions/list.js +25 -0
  19. package/dist/commands/variables/status.js +11 -0
  20. package/dist/config/settings.d.ts +1 -0
  21. package/dist/config/settings.js +6 -0
  22. package/dist/core/types.d.ts +65 -4
  23. package/dist/core/types.js +16 -5
  24. package/dist/env.d.ts +2 -2
  25. package/dist/env.js +1 -1
  26. package/dist/repositories/team.d.ts +10 -2
  27. package/dist/repositories/team.js +53 -3
  28. package/dist/repositories/variable.d.ts +24 -0
  29. package/dist/repositories/variable.js +70 -22
  30. package/dist/runtime/executor.js +2 -2
  31. package/dist/runtime/executor.py +2 -2
  32. package/dist/runtime/libs/python/fcode/__init__.py +2 -2
  33. package/dist/services/auth-service.d.ts +3 -0
  34. package/dist/services/auth-service.js +56 -5
  35. package/dist/services/fcode-graphql-client.d.ts +20 -1
  36. package/dist/services/fcode-graphql-client.js +239 -49
  37. package/dist/services/oauth-browser-service.js +1 -0
  38. package/dist/services/webhook-auth.d.ts +21 -0
  39. package/dist/services/webhook-auth.js +77 -0
  40. package/dist/utils/gitignore.d.ts +2 -0
  41. package/dist/utils/gitignore.js +6 -0
  42. package/oclif.manifest.json +208 -17
  43. package/package.json +10 -2
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/1.5.0 linux-x64 node-v22.22.3
23
+ @factorialco/fcode-cli/2.1.0 linux-x64 node-v22.22.3
24
24
  $ fcode --help [COMMAND]
25
25
  USAGE
26
26
  $ fcode COMMAND
@@ -67,9 +67,15 @@ USAGE
67
67
  * [`fcode settings:set KEY VALUE`](#fcode-settingsset-key-value)
68
68
  * [`fcode setup-debug`](#fcode-setup-debug)
69
69
  * [`fcode status`](#fcode-status)
70
+ * [`fcode team:aliases:delete NAME`](#fcode-teamaliasesdelete-name)
71
+ * [`fcode team:aliases:list`](#fcode-teamaliaseslist)
72
+ * [`fcode team:aliases:set NAME TAG`](#fcode-teamaliasesset-name-tag)
70
73
  * [`fcode team:pull`](#fcode-teampull)
71
74
  * [`fcode team:push`](#fcode-teampush)
72
75
  * [`fcode team:status`](#fcode-teamstatus)
76
+ * [`fcode team:versions:create TAG`](#fcode-teamversionscreate-tag)
77
+ * [`fcode team:versions:delete TAG`](#fcode-teamversionsdelete-tag)
78
+ * [`fcode team:versions:list`](#fcode-teamversionslist)
73
79
  * [`fcode test [SLUG]`](#fcode-test-slug)
74
80
  * [`fcode test:scaffold SLUG`](#fcode-testscaffold-slug)
75
81
  * [`fcode variables:add [RESOURCESLUG]`](#fcode-variablesadd-resourceslug)
@@ -811,6 +817,59 @@ EXAMPLES
811
817
  $ fcode status
812
818
  ```
813
819
 
820
+ ## `fcode team:aliases:delete NAME`
821
+
822
+ Delete a team version alias and the per-entity aliases carrying its name on every owned process and module
823
+
824
+ ```
825
+ USAGE
826
+ $ fcode team:aliases:delete NAME
827
+
828
+ ARGUMENTS
829
+ NAME alias name to delete
830
+
831
+ DESCRIPTION
832
+ Delete a team version alias and the per-entity aliases carrying its name on every owned process and module
833
+
834
+ EXAMPLES
835
+ $ fcode team:aliases:delete production
836
+ ```
837
+
838
+ ## `fcode team:aliases:list`
839
+
840
+ List the team version aliases of the active team workspace
841
+
842
+ ```
843
+ USAGE
844
+ $ fcode team:aliases:list
845
+
846
+ DESCRIPTION
847
+ List the team version aliases of the active team workspace
848
+
849
+ EXAMPLES
850
+ $ fcode team:aliases:list
851
+ ```
852
+
853
+ ## `fcode team:aliases:set NAME TAG`
854
+
855
+ Create or re-point a team version alias, upserting the per-entity alias on every owned process and module that has the target tag published
856
+
857
+ ```
858
+ USAGE
859
+ $ fcode team:aliases:set NAME TAG
860
+
861
+ ARGUMENTS
862
+ NAME alias name (e.g. production)
863
+ TAG team version tag the alias points at
864
+
865
+ DESCRIPTION
866
+ Create or re-point a team version alias, upserting the per-entity alias on every owned process and module that has the
867
+ target tag published
868
+
869
+ EXAMPLES
870
+ $ fcode team:aliases:set production v1.0.0
871
+ ```
872
+
814
873
  ## `fcode team:pull`
815
874
 
816
875
  Fetch team settings (timezone, error handler, inheritance) from cloud into team.json
@@ -828,7 +887,7 @@ EXAMPLES
828
887
 
829
888
  ## `fcode team:push`
830
889
 
831
- Update cloud team settings (timezone, error handler, inheritance) from team.json
890
+ Update cloud team settings (timezone, error handler, inheritance, webhook auth) from team.json
832
891
 
833
892
  ```
834
893
  USAGE
@@ -838,7 +897,7 @@ FLAGS
838
897
  -f, --force Force command
839
898
 
840
899
  DESCRIPTION
841
- Update cloud team settings (timezone, error handler, inheritance) from team.json
900
+ Update cloud team settings (timezone, error handler, inheritance, webhook auth) from team.json
842
901
 
843
902
  EXAMPLES
844
903
  $ fcode team:push
@@ -859,6 +918,67 @@ EXAMPLES
859
918
  $ fcode team:status
860
919
  ```
861
920
 
921
+ ## `fcode team:versions:create TAG`
922
+
923
+ Publish a team version: creates a version with the tag on every owned process and module (existing tags are skipped) and pins bare module imports in the published snapshots
924
+
925
+ ```
926
+ USAGE
927
+ $ fcode team:versions:create TAG [-c <value>]
928
+
929
+ ARGUMENTS
930
+ TAG version tag to publish (e.g. v1.0.0)
931
+
932
+ FLAGS
933
+ -c, --comment=<value> comment describing the version
934
+
935
+ DESCRIPTION
936
+ Publish a team version: creates a version with the tag on every owned process and module (existing tags are skipped)
937
+ and pins bare module imports in the published snapshots
938
+
939
+ EXAMPLES
940
+ $ fcode team:versions:create v1.0.0
941
+
942
+ $ fcode team:versions:create v1.0.0 --comment "First stable release"
943
+ ```
944
+
945
+ ## `fcode team:versions:delete TAG`
946
+
947
+ Delete a team version. CASCADES: every owned process/module version with the tag is deleted, together with the aliases, executions and schedules that reference them
948
+
949
+ ```
950
+ USAGE
951
+ $ fcode team:versions:delete TAG [-f]
952
+
953
+ ARGUMENTS
954
+ TAG version tag to delete
955
+
956
+ FLAGS
957
+ -f, --force Force command
958
+
959
+ DESCRIPTION
960
+ Delete a team version. CASCADES: every owned process/module version with the tag is deleted, together with the
961
+ aliases, executions and schedules that reference them
962
+
963
+ EXAMPLES
964
+ $ fcode team:versions:delete v1.0.0
965
+ ```
966
+
967
+ ## `fcode team:versions:list`
968
+
969
+ List the team versions published on the active team workspace
970
+
971
+ ```
972
+ USAGE
973
+ $ fcode team:versions:list
974
+
975
+ DESCRIPTION
976
+ List the team versions published on the active team workspace
977
+
978
+ EXAMPLES
979
+ $ fcode team:versions:list
980
+ ```
981
+
862
982
  ## `fcode test [SLUG]`
863
983
 
864
984
  Run tests for Factorial Code processes
@@ -11,12 +11,14 @@ export default class Http extends Command {
11
11
  };
12
12
  run(): Promise<void>;
13
13
  /**
14
- * Enforces the process's configured webhook basic auth, simulating the cloud
15
- * webhook endpoint. The username/password come from processes/<slug>/metadata.json;
16
- * an env-var placeholder (e.g. $WEBHOOK_PASSWORD) is resolved against the workspace
17
- * variables. Returns true when the request may proceed, false after writing a 401.
14
+ * Enforces the webhook's configured auth, simulating the cloud webhook endpoint: the effective
15
+ * configuration names a variable, whose value is looked up in the workspace variables and compared
16
+ * against the configured header. A missing or malformed header, an undefined variable, a mismatch,
17
+ * or a webhook inheriting a workspace configuration that team.json does not have, is rejected.
18
+ * Returns true when the request may proceed, false after writing a 403.
18
19
  */
19
20
  private _checkWebhookAuth;
21
+ private _forbid;
20
22
  private _handleProcessExecution;
21
23
  private _sendResponse;
22
24
  }
@@ -7,6 +7,9 @@ const process_runner_1 = require("../services/process-runner");
7
7
  const process_1 = require("../repositories/process");
8
8
  const variable_1 = require("../repositories/variable");
9
9
  const workspace_1 = require("../config/workspace");
10
+ const team_1 = require("../repositories/team");
11
+ const webhook_auth_1 = require("../services/webhook-auth");
12
+ const types_1 = require("../core/types");
10
13
  class Http extends core_1.Command {
11
14
  async run() {
12
15
  const { flags: { port, logLevel, 'auth-user': authUser, 'auth-password': authPassword, jsonLogs }, } = await this.parse(Http);
@@ -16,11 +19,17 @@ class Http extends core_1.Command {
16
19
  : null;
17
20
  const { slug: teamSlug } = await new workspace_1.default().getTeamOrigin();
18
21
  const processRepository = new process_1.default();
22
+ const teamRepository = new team_1.default();
23
+ if (useBasicAuth) {
24
+ this.warn(`--auth-user/--auth-password protect the whole server through the ${types_1.DEFAULT_WEBHOOK_AUTH_HEADER} header, ` +
25
+ `so a webhook expecting its credential there cannot be exercised at the same time.`);
26
+ }
19
27
  const handler = async (req, res) => {
20
28
  // CORS headers
21
29
  res.setHeader('Access-Control-Allow-Origin', '*');
22
30
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
23
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Fcode-Async');
31
+ // Reflected rather than fixed, so a webhook using a custom auth header passes the preflight.
32
+ res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'] ?? 'Content-Type, Authorization, Fcode-Async');
24
33
  if (req.method === 'OPTIONS') {
25
34
  res.writeHead(204);
26
35
  res.end();
@@ -77,7 +86,7 @@ class Http extends core_1.Command {
77
86
  res.end(JSON.stringify({ success: false, error: `Process slug required in path (e.g. /api/${teamSlug}/webhooks/my-process)` }));
78
87
  return;
79
88
  }
80
- if (!(await this._checkWebhookAuth(req, res, processRepository, slug))) {
89
+ if (!(await this._checkWebhookAuth(req, res, processRepository, teamRepository, slug))) {
81
90
  return;
82
91
  }
83
92
  await this._handleProcessExecution(req, res, slug, urlObj, logLevel, jsonLogs);
@@ -110,12 +119,13 @@ class Http extends core_1.Command {
110
119
  await new Promise(() => { });
111
120
  }
112
121
  /**
113
- * Enforces the process's configured webhook basic auth, simulating the cloud
114
- * webhook endpoint. The username/password come from processes/<slug>/metadata.json;
115
- * an env-var placeholder (e.g. $WEBHOOK_PASSWORD) is resolved against the workspace
116
- * variables. Returns true when the request may proceed, false after writing a 401.
122
+ * Enforces the webhook's configured auth, simulating the cloud webhook endpoint: the effective
123
+ * configuration names a variable, whose value is looked up in the workspace variables and compared
124
+ * against the configured header. A missing or malformed header, an undefined variable, a mismatch,
125
+ * or a webhook inheriting a workspace configuration that team.json does not have, is rejected.
126
+ * Returns true when the request may proceed, false after writing a 403.
117
127
  */
118
- async _checkWebhookAuth(req, res, processRepository, slug) {
128
+ async _checkWebhookAuth(req, res, processRepository, teamRepository, slug) {
119
129
  let webhook;
120
130
  try {
121
131
  webhook = (await processRepository.find(slug)).webhook;
@@ -124,28 +134,34 @@ class Http extends core_1.Command {
124
134
  // Unknown process: let the execution path report the 404.
125
135
  return true;
126
136
  }
127
- if (!webhook || (!webhook.username && !webhook.password)) {
128
- return true;
137
+ // Read per request, like the process metadata above, so editing team.json while the server
138
+ // runs takes effect without a restart.
139
+ const teamWebhookAuth = (await teamRepository.find()).webhookAuth;
140
+ let effectiveAuth;
141
+ try {
142
+ effectiveAuth = (0, webhook_auth_1.resolveEffectiveAuth)(webhook, teamWebhookAuth);
129
143
  }
130
- const variables = await new variable_1.default().findAllForExecution();
131
- const values = new Map(variables.map((variable) => [variable.key, variable.value]));
132
- const resolvePlaceholder = (value) => {
133
- if (!value)
134
- return '';
135
- const match = value.match(/^\$\{?([A-Za-z_]\w*)\}?$/);
136
- return match ? values.get(match[1]) ?? value : value;
137
- };
138
- const expected = Buffer.from(`${resolvePlaceholder(webhook.username)}:${resolvePlaceholder(webhook.password)}`).toString('base64');
139
- const authHeader = req.headers['authorization'] ?? '';
140
- const match = authHeader.match(/^Basic (.+)$/);
141
- if (!match || match[1] !== expected) {
142
- res.setHeader('WWW-Authenticate', 'Basic realm="Factorial Code Webhook"');
143
- res.writeHead(401, { 'Content-Type': 'application/json' });
144
- res.end(JSON.stringify({ success: false, error: 'Unauthorized' }));
144
+ catch (error) {
145
+ // Fails closed, and says why: otherwise this reads as a wrong token rather than a local
146
+ // configuration the developer still has to complete.
147
+ this.warn(`Rejecting the ${slug} webhook: ${error.message}`);
148
+ this._forbid(res);
149
+ return false;
150
+ }
151
+ const variables = effectiveAuth === null ? [] : await new variable_1.default().findAllForExecution();
152
+ const variableValue = variables.find((variable) => variable.key === effectiveAuth?.variableKey)?.value;
153
+ const result = (0, webhook_auth_1.checkWebhookAuth)(effectiveAuth, req.headers, variableValue);
154
+ if (!result.authorized) {
155
+ this.warn(`Rejecting the ${slug} webhook: ${result.reason}`);
156
+ this._forbid(res);
145
157
  return false;
146
158
  }
147
159
  return true;
148
160
  }
161
+ _forbid(res) {
162
+ res.writeHead(403, { 'Content-Type': 'application/json' });
163
+ res.end(JSON.stringify({ success: false, error: 'Forbidden' }));
164
+ }
149
165
  async _handleProcessExecution(req, res, slug, urlObj, logLevel, jsonLogs) {
150
166
  if (req.method !== 'GET' && req.method !== 'POST') {
151
167
  res.writeHead(405, { 'Content-Type': 'application/json' });
@@ -70,11 +70,11 @@ class SetupDebug extends core_1.Command {
70
70
  });
71
71
  }
72
72
  }
73
- if (process.env.YC_COMMAND) {
73
+ if (process.env.FC_COMMAND) {
74
74
  configurations.forEach((configuration) => {
75
75
  configuration.env = {
76
76
  ...configuration.env,
77
- YC_COMMAND: process.env.YC_COMMAND
77
+ FC_COMMAND: process.env.FC_COMMAND
78
78
  };
79
79
  });
80
80
  }
@@ -0,0 +1,9 @@
1
+ import ApiClientCommand from '../../../core/commands/api-client-command';
2
+ export default class TeamAliasesDeleteCommand extends ApiClientCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ name: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const color_1 = require("@oclif/color");
5
+ const api_client_command_1 = require("../../../core/commands/api-client-command");
6
+ const pull_1 = require("../pull");
7
+ const cli_1 = require("../../../utils/cli");
8
+ class TeamAliasesDeleteCommand extends api_client_command_1.default {
9
+ async run() {
10
+ const { args: { name }, } = await this.parse(TeamAliasesDeleteCommand);
11
+ const aliases = await this._fcodeGraphQLClient.allTeamVersionAliases();
12
+ const alias = aliases.find((candidate) => candidate.name === name);
13
+ if (!alias) {
14
+ cli_1.default.error(`Alias ${name} does not exist`);
15
+ return;
16
+ }
17
+ cli_1.default.log(color_1.default.cyan(`\n🗑️ deleting alias ${name}...`));
18
+ await this._fcodeGraphQLClient.deleteTeamVersionAlias(alias.id);
19
+ cli_1.default.log(color_1.default.green(`✅ alias ${name} deleted`));
20
+ await pull_1.default.run([]);
21
+ }
22
+ }
23
+ TeamAliasesDeleteCommand.description = 'Delete a team version alias and the per-entity aliases carrying its name on every owned process and module';
24
+ TeamAliasesDeleteCommand.examples = ['$ fcode team:aliases:delete production'];
25
+ TeamAliasesDeleteCommand.args = {
26
+ name: core_1.Args.string({ description: 'alias name to delete', required: true }),
27
+ };
28
+ exports.default = TeamAliasesDeleteCommand;
@@ -0,0 +1,6 @@
1
+ import ApiClientCommand from '../../../core/commands/api-client-command';
2
+ export default class TeamAliasesListCommand extends ApiClientCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const color_1 = require("@oclif/color");
4
+ const api_client_command_1 = require("../../../core/commands/api-client-command");
5
+ const cli_1 = require("../../../utils/cli");
6
+ class TeamAliasesListCommand extends api_client_command_1.default {
7
+ async run() {
8
+ await this.parse(TeamAliasesListCommand);
9
+ const aliases = await this._fcodeGraphQLClient.allTeamVersionAliases();
10
+ if (aliases.length === 0) {
11
+ cli_1.default.log('📋 No team version aliases yet.');
12
+ cli_1.default.log(`Create one with ${color_1.default.cyan('fcode team:aliases:set [name] [tag]')}`);
13
+ return;
14
+ }
15
+ cli_1.default.log('🔗 Team version aliases:\n');
16
+ for (const alias of aliases) {
17
+ cli_1.default.log(` ${color_1.default.cyan(alias.name)} → ${alias.tag}`);
18
+ }
19
+ }
20
+ }
21
+ TeamAliasesListCommand.description = 'List the team version aliases of the active team workspace';
22
+ TeamAliasesListCommand.examples = ['$ fcode team:aliases:list'];
23
+ exports.default = TeamAliasesListCommand;
@@ -0,0 +1,10 @@
1
+ import ApiClientCommand from '../../../core/commands/api-client-command';
2
+ export default class TeamAliasesSetCommand extends ApiClientCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ name: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ tag: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const color_1 = require("@oclif/color");
5
+ const api_client_command_1 = require("../../../core/commands/api-client-command");
6
+ const pull_1 = require("../pull");
7
+ const cli_1 = require("../../../utils/cli");
8
+ class TeamAliasesSetCommand extends api_client_command_1.default {
9
+ async run() {
10
+ const { args: { name, tag }, } = await this.parse(TeamAliasesSetCommand);
11
+ cli_1.default.log(color_1.default.cyan(`\n🔗 pointing alias ${name} at ${tag}...`));
12
+ await this._fcodeGraphQLClient.createTeamVersionAlias(name, tag);
13
+ cli_1.default.log(color_1.default.green(`✅ alias ${name} → ${tag}`));
14
+ // Aliases have no folder representation; refreshing team.json is enough.
15
+ await pull_1.default.run([]);
16
+ }
17
+ }
18
+ TeamAliasesSetCommand.description = 'Create or re-point a team version alias, upserting the per-entity alias on every owned ' +
19
+ 'process and module that has the target tag published';
20
+ TeamAliasesSetCommand.examples = ['$ fcode team:aliases:set production v1.0.0'];
21
+ TeamAliasesSetCommand.args = {
22
+ name: core_1.Args.string({ description: 'alias name (e.g. production)', required: true }),
23
+ tag: core_1.Args.string({ description: 'team version tag the alias points at', required: true }),
24
+ };
25
+ exports.default = TeamAliasesSetCommand;
@@ -12,9 +12,16 @@ class TeamPullCommand extends api_client_command_1.default {
12
12
  async run() {
13
13
  await this.parse(TeamPullCommand);
14
14
  cli_1.default.log(color_1.default.cyan('\n🗄️ pulling team settings...'));
15
- const raw = await this._fcodeGraphQLClient.team();
15
+ const [raw, versions, aliases] = await Promise.all([
16
+ this._fcodeGraphQLClient.team(),
17
+ this._fcodeGraphQLClient.allTeamVersions(),
18
+ this._fcodeGraphQLClient.allTeamVersionAliases(),
19
+ ]);
16
20
  const team = await this._teamRepository.fromRaw(raw);
21
+ this._teamRepository.applyCloudVersioning(team, versions, aliases);
17
22
  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.
18
25
  await this.workspaceConfig.setTeamStoredHash(team.contentHash);
19
26
  }
20
27
  }
@@ -22,6 +22,9 @@ class TeamPushCommand extends api_client_command_1.default {
22
22
  cli_1.default.log('Team settings already up to date');
23
23
  return;
24
24
  }
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.
25
28
  const update = { parentTeamSlugs: team.parentTeamSlugs };
26
29
  if (team.zoneId !== undefined) {
27
30
  update.zoneId = team.zoneId;
@@ -30,12 +33,17 @@ class TeamPushCommand extends api_client_command_1.default {
30
33
  const processId = await this._teamRepository.resolveSlugToId(team.errorHandlerConfig.processSlug);
31
34
  update.executionErrorHandlerConfig = { processId, tag: team.errorHandlerConfig.tag ?? null };
32
35
  }
36
+ // Sent unconditionally, null arm included: team.json is the source of truth, so removing
37
+ // webhookAuth from it must clear the cloud configuration rather than leave it dangling.
38
+ update.webhookAuth = team.webhookAuth
39
+ ? { headerName: team.webhookAuth.headerName ?? null, variableKey: team.webhookAuth.variableKey }
40
+ : { variableKey: null };
33
41
  await this._fcodeGraphQLClient.pushTeam(update);
34
42
  await this.workspaceConfig.setTeamStoredHash(team.contentHash);
35
43
  cli_1.default.log(color_1.default.green('✅ team settings pushed'));
36
44
  }
37
45
  }
38
- TeamPushCommand.description = 'Update cloud team settings (timezone, error handler, inheritance) from team.json';
46
+ TeamPushCommand.description = 'Update cloud team settings (timezone, error handler, inheritance, webhook auth) from team.json';
39
47
  TeamPushCommand.examples = ['$ fcode team:push'];
40
48
  TeamPushCommand.flags = types_1.ForceFlag;
41
49
  exports.default = TeamPushCommand;
@@ -0,0 +1,13 @@
1
+ import ApiClientCommand from '../../../core/commands/api-client-command';
2
+ export default class TeamVersionsCreateCommand extends ApiClientCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ tag: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ comment: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ private logManifest;
13
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const color_1 = require("@oclif/color");
5
+ const api_client_command_1 = require("../../../core/commands/api-client-command");
6
+ const pull_1 = require("../../pull");
7
+ const cli_1 = require("../../../utils/cli");
8
+ class TeamVersionsCreateCommand extends api_client_command_1.default {
9
+ async run() {
10
+ const { args: { tag }, flags: { comment }, } = await this.parse(TeamVersionsCreateCommand);
11
+ cli_1.default.log(color_1.default.cyan(`\n🏷️ publishing team version ${tag}...`));
12
+ const teamVersion = await this._fcodeGraphQLClient.createTeamVersion(tag, comment);
13
+ const failedCount = this.logManifest(teamVersion);
14
+ // Refresh the local workspace: per-entity versions/<tag>/ folders and team.json.
15
+ await pull_1.default.run([]);
16
+ // Release pipelines must be able to detect an incomplete version. Re-running the
17
+ // command converges: it publishes only what is still missing.
18
+ if (failedCount > 0) {
19
+ this.error(`team version ${tag} published with ${failedCount} failed entities`, { exit: 1 });
20
+ }
21
+ }
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
+ }
33
+ TeamVersionsCreateCommand.description = 'Publish a team version: creates a version with the tag on every owned process and module ' +
34
+ '(existing tags are skipped) and pins bare module imports in the published snapshots';
35
+ TeamVersionsCreateCommand.examples = ['$ fcode team:versions:create v1.0.0', '$ fcode team:versions:create v1.0.0 --comment "First stable release"'];
36
+ TeamVersionsCreateCommand.args = {
37
+ tag: core_1.Args.string({ description: 'version tag to publish (e.g. v1.0.0)', required: true }),
38
+ };
39
+ TeamVersionsCreateCommand.flags = {
40
+ comment: core_1.Flags.string({ char: 'c', description: 'comment describing the version' }),
41
+ };
42
+ exports.default = TeamVersionsCreateCommand;
@@ -0,0 +1,12 @@
1
+ import ApiClientCommand from '../../../core/commands/api-client-command';
2
+ export default class TeamVersionsDeleteCommand extends ApiClientCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ tag: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const color_1 = require("@oclif/color");
5
+ const api_client_command_1 = require("../../../core/commands/api-client-command");
6
+ const pull_1 = require("../../pull");
7
+ const types_1 = require("../../../core/types");
8
+ const cli_1 = require("../../../utils/cli");
9
+ class TeamVersionsDeleteCommand extends api_client_command_1.default {
10
+ async run() {
11
+ const { args: { tag }, flags: { force }, } = await this.parse(TeamVersionsDeleteCommand);
12
+ if (!force) {
13
+ const confirmed = await cli_1.default.confirm(`Deleting team version ${tag} also deletes every process/module version with that tag, plus the aliases, executions and schedules referencing them. Continue?`);
14
+ if (!confirmed) {
15
+ return;
16
+ }
17
+ }
18
+ cli_1.default.log(color_1.default.cyan(`\n🗑️ deleting team version ${tag}...`));
19
+ await this._fcodeGraphQLClient.deleteTeamVersion(tag);
20
+ cli_1.default.log(color_1.default.green(`✅ team version ${tag} deleted`));
21
+ // Refresh the local workspace so removed versions/<tag>/ folders disappear.
22
+ await pull_1.default.run([]);
23
+ }
24
+ }
25
+ TeamVersionsDeleteCommand.description = 'Delete a team version. CASCADES: every owned process/module version with the tag is deleted, ' +
26
+ 'together with the aliases, executions and schedules that reference them';
27
+ TeamVersionsDeleteCommand.examples = ['$ fcode team:versions:delete v1.0.0'];
28
+ TeamVersionsDeleteCommand.args = {
29
+ tag: core_1.Args.string({ description: 'version tag to delete', required: true }),
30
+ };
31
+ TeamVersionsDeleteCommand.flags = types_1.ForceFlag;
32
+ exports.default = TeamVersionsDeleteCommand;
@@ -0,0 +1,6 @@
1
+ import ApiClientCommand from '../../../core/commands/api-client-command';
2
+ export default class TeamVersionsListCommand extends ApiClientCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const color_1 = require("@oclif/color");
4
+ const api_client_command_1 = require("../../../core/commands/api-client-command");
5
+ const cli_1 = require("../../../utils/cli");
6
+ class TeamVersionsListCommand extends api_client_command_1.default {
7
+ async run() {
8
+ await this.parse(TeamVersionsListCommand);
9
+ const versions = await this._fcodeGraphQLClient.allTeamVersions();
10
+ if (versions.length === 0) {
11
+ cli_1.default.log('📋 No team versions published yet.');
12
+ cli_1.default.log(`Publish one with ${color_1.default.cyan('fcode team:versions:create [tag]')}`);
13
+ return;
14
+ }
15
+ cli_1.default.log('🏷️ Team versions:\n');
16
+ for (const version of versions) {
17
+ const comment = version.comment ? ` — ${version.comment}` : '';
18
+ const author = version.createdBy?.name ? ` by ${version.createdBy.name}` : '';
19
+ cli_1.default.log(` ${color_1.default.cyan(version.tag)}${comment} (${version.createdAt}${author})`);
20
+ }
21
+ }
22
+ }
23
+ TeamVersionsListCommand.description = 'List the team versions published on the active team workspace';
24
+ TeamVersionsListCommand.examples = ['$ fcode team:versions:list'];
25
+ exports.default = TeamVersionsListCommand;
@@ -11,6 +11,7 @@ class VariablesStatus extends variables_command_1.default {
11
11
  async resourcesTableLog(resources) {
12
12
  const globalConfig = new global_1.default();
13
13
  const localVariablesFileName = await globalConfig.getSetting(settings_1.SettingKeys.LOCAL_VARIABLES_FILE_NAME);
14
+ const hasInherited = resources.some((resource) => resource.resolvingTeamSlug);
14
15
  cli_1.cli.table(resources, {
15
16
  key: {
16
17
  get({ key, status }) {
@@ -18,6 +19,16 @@ class VariablesStatus extends variables_command_1.default {
18
19
  },
19
20
  header: 'key',
20
21
  },
22
+ ...(hasInherited
23
+ ? {
24
+ inherited: {
25
+ get(resource) {
26
+ return (0, colorize_resource_1.colorizeByStatus)(resource.resolvingTeamSlug ? `🔗 ${resource.resolvingTeamSlug}` : '', resource.status);
27
+ },
28
+ header: 'inherited',
29
+ },
30
+ }
31
+ : {}),
21
32
  status: {
22
33
  get({ status }) {
23
34
  return (0, colorize_resource_1.colorizeByStatus)(status, status);
@@ -8,6 +8,7 @@ export declare enum SettingKeys {
8
8
  IGNORE_PROCESS_VERSIONS = "ignoreProcessVersions",
9
9
  IGNORE_WORKSPACE_NOT_EMPTY = "ignoreWorkspaceNotEmpty",
10
10
  IGNORE_FCODE_RUN_PROCESSES = "ignoreFcodeRunProcesses",
11
+ INHERITED_VARIABLES_FILE_NAME = "inheritedVariablesFileName",
11
12
  LOCAL_VARIABLES_FILE_NAME = "localVariablesFileName",
12
13
  SKIP_SKILLS_SETUP = "skipSkillsSetup",
13
14
  VARIABLES_FILE_NAME = "variablesFileName"