@factorialco/fcode-cli 2.1.0 → 2.3.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 +128 -2
- package/dist/commands/add.js +3 -1
- package/dist/commands/clone.d.ts +1 -0
- package/dist/commands/clone.js +19 -1
- package/dist/commands/i18n/add.d.ts +9 -0
- package/dist/commands/i18n/add.js +13 -0
- package/dist/commands/i18n/pull.d.ts +12 -0
- package/dist/commands/i18n/pull.js +14 -0
- package/dist/commands/i18n/push.d.ts +12 -0
- package/dist/commands/i18n/push.js +14 -0
- package/dist/commands/i18n/remove.d.ts +9 -0
- package/dist/commands/i18n/remove.js +19 -0
- package/dist/commands/i18n/reset.d.ts +9 -0
- package/dist/commands/i18n/reset.js +13 -0
- package/dist/commands/i18n/status.d.ts +11 -0
- package/dist/commands/i18n/status.js +42 -0
- package/dist/commands/pull.js +5 -3
- package/dist/commands/push.js +5 -3
- package/dist/commands/reset.js +3 -1
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +5 -2
- package/dist/commands/status.js +4 -2
- package/dist/commands/team/push.js +3 -0
- package/dist/config/remote.js +1 -1
- package/dist/config/workspace.js +10 -0
- 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/commands/i18n-command.d.ts +5 -0
- package/dist/core/commands/i18n-command.js +16 -0
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.js +34 -1
- package/dist/core/executors/execution-task.js +4 -0
- package/dist/core/executors/javascript-execution-task.js +26 -1
- package/dist/core/resource-pusher.js +15 -0
- package/dist/core/types.d.ts +29 -1
- package/dist/core/types.js +46 -3
- package/dist/repositories/locale.d.ts +24 -0
- package/dist/repositories/locale.js +102 -0
- package/dist/repositories/resource.js +4 -1
- package/dist/repositories/team.js +5 -2
- package/dist/runtime/libs/python/fcode/__init__.py +1 -0
- package/dist/runtime/libs/python/fcode/context.py +43 -1
- 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 +13 -1
- package/dist/services/fcode-graphql-client.js +136 -0
- package/dist/services/locale-merger.d.ts +23 -0
- package/dist/services/locale-merger.js +114 -0
- package/dist/services/locale-resolver.d.ts +18 -0
- package/dist/services/locale-resolver.js +120 -0
- package/dist/services/process-runner.d.ts +1 -0
- package/dist/services/process-runner.js +2 -2
- package/dist/services/team-access.d.ts +13 -0
- package/dist/services/team-access.js +45 -0
- package/dist/utils/gitignore.d.ts +1 -1
- package/dist/utils/gitignore.js +4 -1
- package/oclif.manifest.json +194 -1
- package/package.json +2 -1
|
@@ -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})` : ''}'.`));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const locale_1 = require("../../repositories/locale");
|
|
4
|
+
const resource_merger_1 = require("../resource-merger");
|
|
5
|
+
const resource_pusher_1 = require("../resource-pusher");
|
|
6
|
+
const resource_command_1 = require("./resource-command");
|
|
7
|
+
class I18nCommand extends resource_command_1.default {
|
|
8
|
+
async init() {
|
|
9
|
+
await super.init();
|
|
10
|
+
this._resourceRepository = new locale_1.default();
|
|
11
|
+
this._resourceMerger = new resource_merger_1.default(this._resourceRepository);
|
|
12
|
+
this._resourcePusher = new resource_pusher_1.default(this._resourceRepository, this._fcodeGraphQLClient);
|
|
13
|
+
this._resourceQuery = () => this._fcodeGraphQLClient.allLocales.bind(this._fcodeGraphQLClient)();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.default = I18nCommand;
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export declare class LocalVariableNotFound extends ResourceNotFound {
|
|
|
30
30
|
export declare class CloudVariableNotFound extends ResourceNotFound {
|
|
31
31
|
constructor(error: Error | string, variableId: string);
|
|
32
32
|
}
|
|
33
|
+
export declare class LocalLocaleNotFound extends ResourceNotFound {
|
|
34
|
+
constructor(error: Error | string, locale: string);
|
|
35
|
+
}
|
|
36
|
+
export declare class CloudLocaleNotFound extends ResourceNotFound {
|
|
37
|
+
constructor(error: Error | string, locale: string);
|
|
38
|
+
}
|
|
33
39
|
export declare class LocalProgrammingLanguageManifestNotFound extends ResourceNotFound {
|
|
34
40
|
constructor(error: Error | string, programmingLanguageManifestId: string);
|
|
35
41
|
}
|
|
@@ -41,6 +47,10 @@ export declare class NotAuthenticated extends CLIError {
|
|
|
41
47
|
constructor(url: string);
|
|
42
48
|
getUrl(): string;
|
|
43
49
|
}
|
|
50
|
+
export declare function teamAccessDeniedMessage(slug?: string): string;
|
|
51
|
+
export declare class TeamAccessDenied extends CLIError {
|
|
52
|
+
constructor(slug?: string);
|
|
53
|
+
}
|
|
44
54
|
export declare class NotFcodeWorkspace extends CLIError {
|
|
45
55
|
constructor();
|
|
46
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.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) {
|
|
@@ -74,6 +75,24 @@ class CloudVariableNotFound extends ResourceNotFound {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
exports.CloudVariableNotFound = CloudVariableNotFound;
|
|
78
|
+
class LocalLocaleNotFound extends ResourceNotFound {
|
|
79
|
+
constructor(error, locale) {
|
|
80
|
+
super(error, 'Locale', locale, {
|
|
81
|
+
code: 'ERR_LOCAL_LOCALE_NOT_FOUND',
|
|
82
|
+
suggestions: [],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.LocalLocaleNotFound = LocalLocaleNotFound;
|
|
87
|
+
class CloudLocaleNotFound extends ResourceNotFound {
|
|
88
|
+
constructor(error, locale) {
|
|
89
|
+
super(error, 'Locale', locale, {
|
|
90
|
+
code: 'ERR_CLOUD_LOCALE_NOT_FOUND',
|
|
91
|
+
suggestions: [],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.CloudLocaleNotFound = CloudLocaleNotFound;
|
|
77
96
|
class LocalProgrammingLanguageManifestNotFound extends ResourceNotFound {
|
|
78
97
|
constructor(error, programmingLanguageManifestId) {
|
|
79
98
|
super(error, 'ProgrammingLanguageManifest', programmingLanguageManifestId, {
|
|
@@ -102,6 +121,20 @@ class NotAuthenticated extends CLIError {
|
|
|
102
121
|
}
|
|
103
122
|
}
|
|
104
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;
|
|
105
138
|
class NotFcodeWorkspace extends CLIError {
|
|
106
139
|
constructor() {
|
|
107
140
|
super('⚠️ Current directory is not a Factorial Code workspace.', {
|
|
@@ -4,6 +4,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
4
4
|
const fs = require("node:fs");
|
|
5
5
|
const os = require("node:os");
|
|
6
6
|
const path = require("node:path");
|
|
7
|
+
const locale_resolver_1 = require("../../services/locale-resolver");
|
|
7
8
|
const errors_1 = require("../errors");
|
|
8
9
|
const cli_1 = require("../../utils/cli");
|
|
9
10
|
const dependencies_1 = require("../utils/dependencies");
|
|
@@ -114,6 +115,9 @@ class ExecutionTask {
|
|
|
114
115
|
},
|
|
115
116
|
variables: this.getVariablesObject(),
|
|
116
117
|
request: this._options.request ?? 'No request, you are using the CLI',
|
|
118
|
+
// Layered here rather than in the generated runtime: the cloud ships one already-merged
|
|
119
|
+
// flat map, and a local run has to resolve fcode.i18n against the same shape.
|
|
120
|
+
i18n: (0, locale_resolver_1.resolveLocalTranslations)(this._options.locale),
|
|
117
121
|
};
|
|
118
122
|
}
|
|
119
123
|
async prepare() {
|
|
@@ -111,7 +111,12 @@ const main = typeof _hooksModule['${fn}'] === 'function'
|
|
|
111
111
|
};`}
|
|
112
112
|
|
|
113
113
|
global.fcode = {
|
|
114
|
-
|
|
114
|
+
// A version is either a bare string or an options object ({ version: 'v1' }), the
|
|
115
|
+
// form every other versioned helper takes — the cloud reads both, so a local run must too.
|
|
116
|
+
import: (moduleName, tagOrOptions) => {
|
|
117
|
+
const version = typeof tagOrOptions === 'string'
|
|
118
|
+
? tagOrOptions
|
|
119
|
+
: (tagOrOptions && typeof tagOrOptions === 'object' ? tagOrOptions.version : undefined);
|
|
115
120
|
let modulePath = require('path').join('${process.cwd()}', 'modules', moduleName);
|
|
116
121
|
if(version) {
|
|
117
122
|
modulePath = require('path').join(modulePath, 'versions', version)
|
|
@@ -588,6 +593,26 @@ const main = typeof _hooksModule['${fn}'] === 'function'
|
|
|
588
593
|
}
|
|
589
594
|
};
|
|
590
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.
|
|
599
|
+
// Own properties only and no array arguments, exactly like the cloud executors.
|
|
600
|
+
i18n: Object.assign((key, args, _options) => {
|
|
601
|
+
const translations = ${JSON.stringify(metadata.i18n.translations, null, 2)};
|
|
602
|
+
const template = Object.hasOwn(translations, key) ? translations[key] : undefined;
|
|
603
|
+
if (template === undefined) {
|
|
604
|
+
return key;
|
|
605
|
+
}
|
|
606
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
607
|
+
return template;
|
|
608
|
+
}
|
|
609
|
+
// Single pass, so a substituted value containing %{...} is never rescanned.
|
|
610
|
+
return template.replace(/%\{([^}]*)\}/g, (match, name) => {
|
|
611
|
+
const trimmed = name.trim();
|
|
612
|
+
const value = Object.hasOwn(args, trimmed) ? args[trimmed] : undefined;
|
|
613
|
+
return value === undefined || value === null ? match : String(value);
|
|
614
|
+
});
|
|
615
|
+
}, { locale: ${JSON.stringify(metadata.i18n.locale ?? null)} }),
|
|
591
616
|
sendMail: async ({ to, subject, text, html } = {}) => {
|
|
592
617
|
// No manager locally: log the email contents instead of sending, so
|
|
593
618
|
// devs can see exactly what would be delivered when run in the cloud.
|
|
@@ -82,6 +82,16 @@ class ResourcePusher {
|
|
|
82
82
|
const createdVariable = await this._fcodeGraphQLClient.pushVariable(variable);
|
|
83
83
|
return this._repository.save(createdVariable);
|
|
84
84
|
}
|
|
85
|
+
if (this._repository._repositoryName === 'i18n') {
|
|
86
|
+
const locale = resource;
|
|
87
|
+
// The recorded id is only a hint: pushing an identifier a parent workspace owns has to
|
|
88
|
+
// create an override here, so the update path is taken only for a locale this workspace
|
|
89
|
+
// already owns.
|
|
90
|
+
const pushed = locale.id && !locale.resolvingTeamSlug
|
|
91
|
+
? await this._fcodeGraphQLClient.updateLocale(locale)
|
|
92
|
+
: await this._fcodeGraphQLClient.pushLocale(locale);
|
|
93
|
+
return this._repository.save(pushed);
|
|
94
|
+
}
|
|
85
95
|
if (this._repository._repositoryName === 'dependencies') {
|
|
86
96
|
const updatedProgrammingLanguageManifests = await this._fcodeGraphQLClient.pushProgrammingLanguageManifest(resource);
|
|
87
97
|
return this._repository.save(updatedProgrammingLanguageManifests);
|
|
@@ -114,6 +124,11 @@ class ResourcePusher {
|
|
|
114
124
|
if (this._repository._repositoryName === 'variables') {
|
|
115
125
|
await this._fcodeGraphQLClient.removeVariable(resource.id);
|
|
116
126
|
}
|
|
127
|
+
if (this._repository._repositoryName === 'i18n') {
|
|
128
|
+
// By identifier, not slug: an inherited locale is tracked under a qualified slug, and only
|
|
129
|
+
// this workspace's own locales — where the two coincide — ever reach a remote delete.
|
|
130
|
+
await this._fcodeGraphQLClient.removeLocale(resource.locale);
|
|
131
|
+
}
|
|
117
132
|
this._repository.removeMetadata(resource.slug);
|
|
118
133
|
}
|
|
119
134
|
catch (error) {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -41,6 +41,9 @@ export interface TeamMetadata {
|
|
|
41
41
|
variables: {
|
|
42
42
|
[key: string]: Variable;
|
|
43
43
|
};
|
|
44
|
+
i18n: {
|
|
45
|
+
[key: string]: Locale;
|
|
46
|
+
};
|
|
44
47
|
dependencies: {
|
|
45
48
|
[key: string]: ProgrammingLanguageManifest;
|
|
46
49
|
};
|
|
@@ -187,6 +190,27 @@ export declare class ScriptLibrary extends Resource {
|
|
|
187
190
|
constructor(id: string, name: string, sourceCode: string, programmingLanguage: ProgrammingLanguage, resolvingTeamSlug?: string);
|
|
188
191
|
get contentHash(): string;
|
|
189
192
|
}
|
|
193
|
+
export declare class Locale extends Resource {
|
|
194
|
+
static LOCALE_REGEX: RegExp;
|
|
195
|
+
static INHERITED_SUFFIX: string;
|
|
196
|
+
id: string | undefined;
|
|
197
|
+
slug: string;
|
|
198
|
+
locale: string;
|
|
199
|
+
content: string;
|
|
200
|
+
constructor(id: string | undefined, locale: string, content: string, resolvingTeamSlug?: string);
|
|
201
|
+
/**
|
|
202
|
+
* The name a locale is tracked and stored under. Unlike every other resource, the identifier is
|
|
203
|
+
* not unique: a workspace sees its own `en` and each parent workspace's `en`, and all of them are
|
|
204
|
+
* live, because an override is layered over the parent's file key by key rather than replacing
|
|
205
|
+
* it. Naming files after the identifier alone would keep only one of them.
|
|
206
|
+
*
|
|
207
|
+
* A workspace directory therefore holds two files at most per identifier: `en.yaml` for its own
|
|
208
|
+
* and `en.inherited.yaml` for everything it inherits, which `mergeInheritedLocales` collapses
|
|
209
|
+
* into one file. An identifier cannot contain a dot, so the name parses back.
|
|
210
|
+
*/
|
|
211
|
+
static slugFor(locale: string, resolvingTeamSlug?: string): string;
|
|
212
|
+
get contentHash(): string;
|
|
213
|
+
}
|
|
190
214
|
export declare class VersionedScriptLibrary extends Resource {
|
|
191
215
|
id: string;
|
|
192
216
|
slug: string;
|
|
@@ -262,9 +286,10 @@ export declare class Team extends Resource {
|
|
|
262
286
|
errorHandlerConfig?: ErrorHandlerConfig;
|
|
263
287
|
parentTeamSlugs: string[];
|
|
264
288
|
webhookAuth?: WebhookAuth;
|
|
289
|
+
primaryLocale?: string;
|
|
265
290
|
versions?: TeamVersionFileEntry[];
|
|
266
291
|
aliases?: TeamAliasFileEntry[];
|
|
267
|
-
constructor(slug: string, parentTeamSlugs: string[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig, webhookAuth?: WebhookAuth);
|
|
292
|
+
constructor(slug: string, parentTeamSlugs: string[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig, webhookAuth?: WebhookAuth, primaryLocale?: string);
|
|
268
293
|
get contentHash(): string;
|
|
269
294
|
}
|
|
270
295
|
export interface RawErrorHandlerConfig {
|
|
@@ -277,6 +302,7 @@ export interface RawTeamSettings {
|
|
|
277
302
|
errorHandlerConfig?: RawErrorHandlerConfig | null;
|
|
278
303
|
parentTeamSlugs: string[];
|
|
279
304
|
webhookAuth?: WebhookAuth | null;
|
|
305
|
+
primaryLocale?: string | null;
|
|
280
306
|
}
|
|
281
307
|
export interface TeamUpdateInput {
|
|
282
308
|
zoneId?: string;
|
|
@@ -286,6 +312,7 @@ export interface TeamUpdateInput {
|
|
|
286
312
|
headerName?: string | null;
|
|
287
313
|
variableKey?: string | null;
|
|
288
314
|
} | null;
|
|
315
|
+
primaryLocale?: string;
|
|
289
316
|
}
|
|
290
317
|
export type ResourceMetadata = {
|
|
291
318
|
[key: string]: any;
|
|
@@ -317,6 +344,7 @@ export type ExecutionOptions = {
|
|
|
317
344
|
hookFilePath?: string;
|
|
318
345
|
hookFunction?: string;
|
|
319
346
|
silent?: boolean;
|
|
347
|
+
locale?: string;
|
|
320
348
|
};
|
|
321
349
|
export declare const ForceFlag: {
|
|
322
350
|
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
package/dist/core/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ServiceAccount = exports.Dependency = exports.DependenciesFlags = exports.TeamNameArg = exports.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.Team = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedScriptLibrary = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = exports.ResourceStatus = exports.OnConflictResolution = exports.Resource = void 0;
|
|
3
|
+
exports.ServiceAccount = exports.Dependency = exports.DependenciesFlags = exports.TeamNameArg = exports.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.Team = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedScriptLibrary = exports.Locale = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = exports.ResourceStatus = exports.OnConflictResolution = exports.Resource = void 0;
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
const crypto = require("node:crypto");
|
|
6
6
|
const canonical_hash_1 = require("../utils/canonical-hash");
|
|
@@ -103,6 +103,41 @@ class ScriptLibrary extends Resource {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
exports.ScriptLibrary = ScriptLibrary;
|
|
106
|
+
class Locale extends Resource {
|
|
107
|
+
constructor(id, locale, content, resolvingTeamSlug) {
|
|
108
|
+
super();
|
|
109
|
+
this.id = id;
|
|
110
|
+
this.slug = Locale.slugFor(locale, resolvingTeamSlug);
|
|
111
|
+
this.locale = locale;
|
|
112
|
+
this.content = content;
|
|
113
|
+
this.resolvingTeamSlug = resolvingTeamSlug;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The name a locale is tracked and stored under. Unlike every other resource, the identifier is
|
|
117
|
+
* not unique: a workspace sees its own `en` and each parent workspace's `en`, and all of them are
|
|
118
|
+
* live, because an override is layered over the parent's file key by key rather than replacing
|
|
119
|
+
* it. Naming files after the identifier alone would keep only one of them.
|
|
120
|
+
*
|
|
121
|
+
* A workspace directory therefore holds two files at most per identifier: `en.yaml` for its own
|
|
122
|
+
* and `en.inherited.yaml` for everything it inherits, which `mergeInheritedLocales` collapses
|
|
123
|
+
* into one file. An identifier cannot contain a dot, so the name parses back.
|
|
124
|
+
*/
|
|
125
|
+
static slugFor(locale, resolvingTeamSlug) {
|
|
126
|
+
return resolvingTeamSlug ? `${locale}${Locale.INHERITED_SUFFIX}` : locale;
|
|
127
|
+
}
|
|
128
|
+
// Hashed over the raw YAML: the file is the source of truth and round-trips comments and
|
|
129
|
+
// ordering, so any edit — even one that parses to the same translations — is a change to push.
|
|
130
|
+
get contentHash() {
|
|
131
|
+
return crypto.createHash('md5').update(this.content).digest('hex');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.Locale = Locale;
|
|
135
|
+
// Free-form identifier rather than a validated BCP-47 tag, so a workspace can use whatever it
|
|
136
|
+
// already names its translations after. Case is significant: pt-BR and pt-br are different
|
|
137
|
+
// locales, and the platform's unique index is case-sensitive too.
|
|
138
|
+
Locale.LOCALE_REGEX = /^[A-Za-z0-9][\w-]{0,19}$/;
|
|
139
|
+
// Marks a file as owned by a parent workspace, the way `variables.inherited.env` does.
|
|
140
|
+
Locale.INHERITED_SUFFIX = '.inherited';
|
|
106
141
|
class VersionedScriptLibrary extends Resource {
|
|
107
142
|
constructor(id, tag, sourceCode, programmingLanguage) {
|
|
108
143
|
super();
|
|
@@ -166,19 +201,22 @@ class ProgrammingLanguageManifest extends Resource {
|
|
|
166
201
|
}
|
|
167
202
|
exports.ProgrammingLanguageManifest = ProgrammingLanguageManifest;
|
|
168
203
|
class Team extends Resource {
|
|
169
|
-
constructor(slug, parentTeamSlugs, zoneId, errorHandlerConfig, webhookAuth) {
|
|
204
|
+
constructor(slug, parentTeamSlugs, zoneId, errorHandlerConfig, webhookAuth, primaryLocale) {
|
|
170
205
|
super();
|
|
171
206
|
this.slug = slug;
|
|
172
207
|
this.parentTeamSlugs = parentTeamSlugs;
|
|
173
208
|
this.zoneId = zoneId;
|
|
174
209
|
this.errorHandlerConfig = errorHandlerConfig;
|
|
175
210
|
this.webhookAuth = webhookAuth;
|
|
211
|
+
this.primaryLocale = primaryLocale;
|
|
176
212
|
}
|
|
177
213
|
get contentHash() {
|
|
178
214
|
const payload = {
|
|
179
215
|
zoneId: this.zoneId || null,
|
|
180
216
|
errorHandlerConfig: this.errorHandlerConfig || null,
|
|
181
|
-
|
|
217
|
+
// Kept in the given order, not sorted: the order is the inheritance precedence, so a pure
|
|
218
|
+
// reorder is a real change that must be detected and pushed.
|
|
219
|
+
parentTeamSlugs: [...(this.parentTeamSlugs || [])],
|
|
182
220
|
};
|
|
183
221
|
// Inserted only when set, so every team.json that predates webhook auth keeps the
|
|
184
222
|
// hash already stored in .fcode and does not report a phantom conflict. A key present
|
|
@@ -186,6 +224,11 @@ class Team extends Resource {
|
|
|
186
224
|
if (this.webhookAuth) {
|
|
187
225
|
payload.webhookAuth = this.webhookAuth;
|
|
188
226
|
}
|
|
227
|
+
// Same insert-only-when-set rule, for the same reason: a team.json written before locales
|
|
228
|
+
// existed must keep the hash already stored in .fcode.
|
|
229
|
+
if (this.primaryLocale) {
|
|
230
|
+
payload.primaryLocale = this.primaryLocale;
|
|
231
|
+
}
|
|
189
232
|
return (0, canonical_hash_1.canonicalJsonHash)(payload);
|
|
190
233
|
}
|
|
191
234
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Locale, ResourceMetadata } from '../core/types';
|
|
2
|
+
import ResourceRepositoryBase from './resource';
|
|
3
|
+
/**
|
|
4
|
+
* Locales live as one YAML file per locale directly under `i18n/`, named after the slug
|
|
5
|
+
* (`i18n/pt-BR.yaml`, `i18n/pt-BR.inherited.yaml`). A file, not a directory: a locale has exactly
|
|
6
|
+
* one artifact, and naming the file after the identifier is what lets `push` work without knowing
|
|
7
|
+
* any ids.
|
|
8
|
+
*
|
|
9
|
+
* A workspace keeps what it inherits next to what it owns, because overriding a locale only layers
|
|
10
|
+
* keys over the inherited file rather than replacing it — see {@link Locale.slugFor}.
|
|
11
|
+
*/
|
|
12
|
+
declare class LocaleRepository extends ResourceRepositoryBase<Locale> {
|
|
13
|
+
constructor();
|
|
14
|
+
find(slug: string): Promise<Locale>;
|
|
15
|
+
findSlugsNotAdded(): Promise<Array<string>>;
|
|
16
|
+
protected getResourcePath(locale: Locale): string;
|
|
17
|
+
protected removeFiles(locale: Locale): Promise<void>;
|
|
18
|
+
protected resourceToMetadata(locale: Locale): ResourceMetadata;
|
|
19
|
+
protected saveFiles(locale: Locale): Promise<void>;
|
|
20
|
+
metadataFromFiles(slug: string): Promise<ResourceMetadata>;
|
|
21
|
+
validSlug(slug: string): boolean;
|
|
22
|
+
slugFormatMessage(): string;
|
|
23
|
+
}
|
|
24
|
+
export default LocaleRepository;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const color_1 = require("@oclif/color");
|
|
6
|
+
const cli_1 = require("../utils/cli");
|
|
7
|
+
const errors_1 = require("../core/errors");
|
|
8
|
+
const types_1 = require("../core/types");
|
|
9
|
+
const file_utils_1 = require("../utils/file-utils");
|
|
10
|
+
const resource_1 = require("./resource");
|
|
11
|
+
const YAML_EXTENSION = '.yaml';
|
|
12
|
+
/**
|
|
13
|
+
* Locales live as one YAML file per locale directly under `i18n/`, named after the slug
|
|
14
|
+
* (`i18n/pt-BR.yaml`, `i18n/pt-BR.inherited.yaml`). A file, not a directory: a locale has exactly
|
|
15
|
+
* one artifact, and naming the file after the identifier is what lets `push` work without knowing
|
|
16
|
+
* any ids.
|
|
17
|
+
*
|
|
18
|
+
* A workspace keeps what it inherits next to what it owns, because overriding a locale only layers
|
|
19
|
+
* keys over the inherited file rather than replacing it — see {@link Locale.slugFor}.
|
|
20
|
+
*/
|
|
21
|
+
class LocaleRepository extends resource_1.default {
|
|
22
|
+
constructor() {
|
|
23
|
+
super({
|
|
24
|
+
name: 'i18n',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async find(slug) {
|
|
28
|
+
const { [this._repositoryName]: locales } = await this.workspaceConfig.getActiveTeam();
|
|
29
|
+
const localeMetadata = locales?.[slug];
|
|
30
|
+
if (!localeMetadata) {
|
|
31
|
+
throw new errors_1.LocalLocaleNotFound(new Error('Locale not found on team metadata'), slug);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(this.getResourcePath(localeMetadata)).toString();
|
|
35
|
+
return new types_1.Locale(localeMetadata.id, localeMetadata.locale ?? slug, content, localeMetadata.resolvingTeamSlug);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new errors_1.LocalLocaleNotFound(error, slug);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async findSlugsNotAdded() {
|
|
42
|
+
const slugsNotAdded = [];
|
|
43
|
+
if (!fs.pathExistsSync(this._repositoryName)) {
|
|
44
|
+
return slugsNotAdded;
|
|
45
|
+
}
|
|
46
|
+
for (const fileName of fs.readdirSync(this._repositoryName)) {
|
|
47
|
+
if (!fileName.endsWith(YAML_EXTENSION)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const slug = path.basename(fileName, YAML_EXTENSION);
|
|
51
|
+
if (await this.workspaceConfig.exists('i18n', slug)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Only `pull` writes an inherited file, and it registers it as it does. One left behind by a
|
|
55
|
+
// workspace that no longer inherits it is stale, not a locale waiting to be tracked.
|
|
56
|
+
if (slug.endsWith(types_1.Locale.INHERITED_SUFFIX)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (this.validSlug(slug)) {
|
|
60
|
+
slugsNotAdded.push(slug);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
cli_1.default.log(color_1.default.yellow(`\n⚠️ local locale file '${fileName}' doesn't have a valid name and will be ignored. ${this.slugFormatMessage()}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return slugsNotAdded;
|
|
67
|
+
}
|
|
68
|
+
getResourcePath(locale) {
|
|
69
|
+
return path.join(this._repositoryName, `${locale.slug ?? locale.locale}${YAML_EXTENSION}`);
|
|
70
|
+
}
|
|
71
|
+
async removeFiles(locale) {
|
|
72
|
+
fs.removeSync(this.getResourcePath(locale));
|
|
73
|
+
this.removeDirectoryIfEmpty(this._repositoryName);
|
|
74
|
+
}
|
|
75
|
+
resourceToMetadata(locale) {
|
|
76
|
+
return {
|
|
77
|
+
id: locale.id,
|
|
78
|
+
locale: locale.locale,
|
|
79
|
+
slug: locale.slug,
|
|
80
|
+
...(locale.resolvingTeamSlug && { resolvingTeamSlug: locale.resolvingTeamSlug }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async saveFiles(locale) {
|
|
84
|
+
// Inherited locales are written read-only, like inherited modules: they belong to a parent
|
|
85
|
+
// workspace, and editing one here would push an override rather than change the parent.
|
|
86
|
+
(0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(locale), locale.content || '', { readOnly: Boolean(locale.resolvingTeamSlug) });
|
|
87
|
+
}
|
|
88
|
+
async metadataFromFiles(slug) {
|
|
89
|
+
return {
|
|
90
|
+
locale: slug,
|
|
91
|
+
slug: slug,
|
|
92
|
+
contentHash: '',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
validSlug(slug) {
|
|
96
|
+
return types_1.Locale.LOCALE_REGEX.test(slug);
|
|
97
|
+
}
|
|
98
|
+
slugFormatMessage() {
|
|
99
|
+
return "Use up to 20 letters, numbers, '-' or '_', starting with a letter or a number.";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.default = LocaleRepository;
|
|
@@ -19,7 +19,10 @@ class ResourceRepositoryBase {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
async findAll() {
|
|
22
|
-
|
|
22
|
+
// Defaulted because a collection added after a workspace was cloned (as `i18n` was) is absent
|
|
23
|
+
// from its sync metadata until the first pull writes it — findAll must see "no resources",
|
|
24
|
+
// not crash every aggregate pull/push/status in every pre-existing workspace.
|
|
25
|
+
const { [this._repositoryName]: resources = {} } = await this.workspaceConfig.getActiveTeam();
|
|
23
26
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
24
27
|
const existResources = await Object.keys(resources).reduce((promise, slug) => promise.then(async (existIds) => {
|
|
25
28
|
const exists = await this.exists(slug);
|
|
@@ -29,7 +29,7 @@ class TeamRepository {
|
|
|
29
29
|
}
|
|
30
30
|
catch { }
|
|
31
31
|
const parentTeamSlugs = Array.isArray(raw.parentTeamSlugs) ? raw.parentTeamSlugs : [];
|
|
32
|
-
const team = new types_1.Team(slug, parentTeamSlugs, raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig), this.parseWebhookAuth(raw.webhookAuth));
|
|
32
|
+
const team = new types_1.Team(slug, parentTeamSlugs, raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig), this.parseWebhookAuth(raw.webhookAuth), typeof raw.primaryLocale === 'string' && raw.primaryLocale.length > 0 ? raw.primaryLocale : undefined);
|
|
33
33
|
// Parsed so a find()+save() round-trip does not drop the pull-only fields.
|
|
34
34
|
if (Array.isArray(raw.versions)) {
|
|
35
35
|
team.versions = raw.versions;
|
|
@@ -51,7 +51,7 @@ class TeamRepository {
|
|
|
51
51
|
tag: raw.errorHandlerConfig.tag ?? null,
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
return new types_1.Team(raw.slug, raw.parentTeamSlugs ?? [], raw.zoneId, errorHandlerConfig, this.parseWebhookAuth(raw.webhookAuth));
|
|
54
|
+
return new types_1.Team(raw.slug, raw.parentTeamSlugs ?? [], raw.zoneId, errorHandlerConfig, this.parseWebhookAuth(raw.webhookAuth), raw.primaryLocale ?? undefined);
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* Writes the cloud's team versions and aliases into the Team as the pull-only team.json
|
|
@@ -111,6 +111,9 @@ class TeamRepository {
|
|
|
111
111
|
if (team.webhookAuth !== undefined) {
|
|
112
112
|
file.webhookAuth = team.webhookAuth;
|
|
113
113
|
}
|
|
114
|
+
if (team.primaryLocale !== undefined) {
|
|
115
|
+
file.primaryLocale = team.primaryLocale;
|
|
116
|
+
}
|
|
114
117
|
// Pull-only global versioning fields, appended after the pushable settings.
|
|
115
118
|
if (team.versions !== undefined) {
|
|
116
119
|
file.versions = team.versions;
|
|
@@ -78,8 +78,42 @@ class Processes(dict):
|
|
|
78
78
|
print(error, file=sys.stderr)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
class I18n:
|
|
82
|
+
"""Local equivalent of the cloud's fcode.i18n.
|
|
83
|
+
|
|
84
|
+
The CLI has already layered the chosen locale over the workspace's primary one, so what
|
|
85
|
+
arrives here is a single flat map. Nothing raises: a key with no translation resolves to the
|
|
86
|
+
key itself, and a placeholder with no argument is left exactly as written.
|
|
87
|
+
|
|
88
|
+
The ``options`` argument (the version a call pins in the cloud) is accepted and ignored: a
|
|
89
|
+
workspace directory holds no published snapshots, so a pinned call resolves against the files
|
|
90
|
+
on disk -- the same per-file fallback the cloud applies to a locale never published at that tag.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, i18n_payload):
|
|
94
|
+
payload = i18n_payload or {}
|
|
95
|
+
self.__translations = payload.get("translations") or {}
|
|
96
|
+
self.locale = payload.get("locale")
|
|
97
|
+
|
|
98
|
+
def __call__(self, key, args=None, options=None):
|
|
99
|
+
template = self.__translations.get(key)
|
|
100
|
+
if template is None:
|
|
101
|
+
return key
|
|
102
|
+
if not isinstance(args, dict):
|
|
103
|
+
return template
|
|
104
|
+
|
|
105
|
+
import re
|
|
106
|
+
|
|
107
|
+
def replace(match):
|
|
108
|
+
value = args.get(match.group(1).strip())
|
|
109
|
+
return match.group(0) if value is None else str(value)
|
|
110
|
+
|
|
111
|
+
# Single pass, so a substituted value containing %{...} is never rescanned.
|
|
112
|
+
return re.sub(r"%\{([^}]*)\}", replace, template)
|
|
113
|
+
|
|
114
|
+
|
|
81
115
|
class FcodeGlobal:
|
|
82
|
-
def __init__(self, context, execution, env, datastore, storage, schedule, variables):
|
|
116
|
+
def __init__(self, context, execution, env, datastore, storage, schedule, variables, i18n=None):
|
|
83
117
|
self.context = context
|
|
84
118
|
self.execution = execution
|
|
85
119
|
self.team = AttrsInfo({"timezone": "UTC"})
|
|
@@ -89,6 +123,7 @@ class FcodeGlobal:
|
|
|
89
123
|
self.schedule = schedule
|
|
90
124
|
self.variables = variables
|
|
91
125
|
self.processes = Processes()
|
|
126
|
+
self.i18n = I18n(i18n)
|
|
92
127
|
|
|
93
128
|
def send_mail(self, to=None, subject=None, text=None, html=None):
|
|
94
129
|
# No manager locally: log the email contents instead of sending, so devs
|
|
@@ -110,6 +145,13 @@ class FcodeGlobal:
|
|
|
110
145
|
return {"messageId": "local-run", "accepted": accepted, "rejected": []}
|
|
111
146
|
|
|
112
147
|
def import_module(self, module_name, version=None):
|
|
148
|
+
"""Import a workspace module, optionally at a version.
|
|
149
|
+
|
|
150
|
+
The version is either a bare string or an options dict ({"version": "v1"}), the form
|
|
151
|
+
every other versioned helper takes — the cloud reads both, so a local run must too.
|
|
152
|
+
"""
|
|
153
|
+
if isinstance(version, dict):
|
|
154
|
+
version = version.get("version")
|
|
113
155
|
module_path = module_name
|
|
114
156
|
if version:
|
|
115
157
|
module_path = os.path.join(module_path, "versions", version)
|