@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.
- package/README.md +12 -8
- package/dist/commands/clone.js +14 -1
- package/dist/commands/dependencies/status.d.ts +11 -0
- package/dist/commands/dependencies/status.js +43 -0
- package/dist/commands/pull.js +1 -1
- package/dist/commands/team/pull.d.ts +3 -0
- package/dist/commands/team/pull.js +15 -4
- package/dist/commands/team/push.d.ts +2 -0
- package/dist/commands/team/push.js +55 -4
- package/dist/commands/team/status.js +8 -2
- package/dist/commands/team/versions/create.d.ts +0 -1
- package/dist/commands/team/versions/create.js +2 -11
- package/dist/core/commands/api-client-command.d.ts +1 -0
- package/dist/core/commands/api-client-command.js +5 -0
- package/dist/core/commands/base-command.d.ts +2 -0
- package/dist/core/commands/base-command.js +31 -0
- package/dist/core/dependencies/env-dependencies-command.js +6 -3
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.js +16 -1
- package/dist/core/executors/javascript-execution-task.js +14 -5
- package/dist/core/types.d.ts +29 -2
- package/dist/core/types.js +30 -2
- package/dist/core/utils/dependencies.js +4 -1
- package/dist/env.js +1 -1
- package/dist/repositories/process.js +4 -0
- package/dist/repositories/programming-language-manifest.d.ts +17 -1
- package/dist/repositories/programming-language-manifest.js +51 -1
- package/dist/repositories/team.d.ts +13 -3
- package/dist/repositories/team.js +51 -8
- package/dist/runtime/libs/python/fcode/context.py +15 -8
- package/dist/services/auth-service.d.ts +1 -0
- package/dist/services/auth-service.js +12 -0
- package/dist/services/fcode-graphql-client.d.ts +3 -0
- package/dist/services/fcode-graphql-client.js +97 -9
- package/dist/services/locale-merger.d.ts +1 -1
- package/dist/services/locale-merger.js +16 -13
- package/dist/services/locale-resolver.d.ts +6 -1
- package/dist/services/locale-resolver.js +11 -4
- package/dist/services/team-access.d.ts +13 -0
- package/dist/services/team-access.js +45 -0
- package/dist/services/team-versioning.d.ts +16 -0
- package/dist/services/team-versioning.js +40 -0
- package/dist/utils/gitignore.d.ts +3 -1
- package/dist/utils/gitignore.js +11 -2
- package/oclif.manifest.json +13 -5
- 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.
|
|
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)
|
|
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
|
package/dist/commands/clone.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/commands/pull.js
CHANGED
|
@@ -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;
|
|
@@ -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)
|
|
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
|
|
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;
|
|
@@ -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 =
|
|
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';
|
|
@@ -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
|
-
|
|
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,
|
|
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()).
|
|
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
|
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/core/errors.js
CHANGED
|
@@ -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
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
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,
|
|
601
|
+
i18n: Object.assign((key, args, options) => {
|
|
601
602
|
const translations = ${JSON.stringify(metadata.i18n.translations, null, 2)};
|
|
602
|
-
const
|
|
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
|
}
|