@factorialco/fcode-cli 2.3.0 → 3.0.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 +334 -117
- package/dist/commands/clone.d.ts +0 -12
- package/dist/commands/clone.js +22 -135
- package/dist/commands/{settings → config}/get.d.ts +1 -1
- package/dist/commands/{settings → config}/get.js +7 -7
- package/dist/commands/{settings → config}/remove.d.ts +1 -1
- package/dist/commands/{settings → config}/remove.js +6 -6
- package/dist/commands/{settings → config}/set.d.ts +1 -1
- package/dist/commands/{settings → config}/set.js +8 -8
- package/dist/commands/dependencies/diff.d.ts +9 -0
- package/dist/commands/dependencies/diff.js +15 -0
- package/dist/commands/dependencies/pull.d.ts +9 -0
- package/dist/commands/dependencies/pull.js +44 -1
- package/dist/commands/dependencies/status.d.ts +11 -0
- package/dist/commands/dependencies/status.js +43 -0
- package/dist/commands/diff.d.ts +12 -0
- package/dist/commands/diff.js +48 -0
- package/dist/commands/http.d.ts +1 -1
- package/dist/commands/http.js +7 -7
- package/dist/commands/i18n/diff.d.ts +9 -0
- package/dist/commands/i18n/diff.js +15 -0
- package/dist/commands/i18n/status.d.ts +3 -0
- package/dist/commands/i18n/status.js +1 -0
- package/dist/commands/modules/diff.d.ts +9 -0
- package/dist/commands/modules/diff.js +15 -0
- package/dist/commands/modules/status.d.ts +3 -0
- package/dist/commands/modules/status.js +1 -0
- package/dist/commands/processes/diff.d.ts +9 -0
- package/dist/commands/processes/diff.js +15 -0
- package/dist/commands/processes/status.d.ts +3 -0
- package/dist/commands/processes/status.js +1 -0
- package/dist/commands/pull.js +3 -3
- package/dist/commands/push.js +2 -2
- package/dist/commands/{team → settings}/aliases/delete.d.ts +1 -1
- package/dist/commands/{team → settings}/aliases/delete.js +6 -6
- package/dist/commands/{team → settings}/aliases/list.d.ts +1 -1
- package/dist/commands/{team → settings}/aliases/list.js +8 -8
- package/dist/commands/{team → settings}/aliases/set.d.ts +1 -1
- package/dist/commands/{team → settings}/aliases/set.js +8 -8
- package/dist/commands/settings/diff.d.ts +8 -0
- package/dist/commands/settings/diff.js +39 -0
- package/dist/commands/{team/push.d.ts → settings/pull.d.ts} +3 -3
- package/dist/commands/settings/pull.js +41 -0
- package/dist/commands/settings/push.d.ts +13 -0
- package/dist/commands/settings/push.js +103 -0
- package/dist/commands/settings/status.d.ts +8 -0
- package/dist/commands/settings/status.js +46 -0
- package/dist/commands/{team → settings}/versions/create.d.ts +1 -2
- package/dist/commands/settings/versions/create.js +36 -0
- package/dist/commands/{team → settings}/versions/delete.d.ts +1 -1
- package/dist/commands/{team → settings}/versions/delete.js +10 -10
- package/dist/commands/{team → settings}/versions/list.d.ts +1 -1
- package/dist/commands/{team → settings}/versions/list.js +8 -8
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +10 -7
- package/dist/commands/team/clone.d.ts +14 -0
- package/dist/commands/team/clone.js +91 -0
- package/dist/commands/team/pull.d.ts +3 -4
- package/dist/commands/team/pull.js +25 -19
- package/dist/commands/team/status.d.ts +3 -4
- package/dist/commands/team/status.js +34 -27
- package/dist/commands/variables/diff.d.ts +9 -0
- package/dist/commands/variables/diff.js +15 -0
- package/dist/commands/variables/status.d.ts +3 -0
- package/dist/commands/variables/status.js +1 -0
- package/dist/config/dev-team.d.ts +48 -0
- package/dist/config/dev-team.js +62 -0
- package/dist/config/workspace.d.ts +2 -2
- package/dist/config/workspace.js +4 -2
- package/dist/core/commands/base-command.js +5 -0
- package/dist/core/commands/dependency-command.d.ts +2 -1
- package/dist/core/commands/dependency-command.js +3 -0
- package/dist/core/commands/i18n-command.js +2 -0
- package/dist/core/commands/modules-command.js +2 -0
- package/dist/core/commands/processes-command.js +2 -0
- package/dist/core/commands/resource-command.d.ts +26 -1
- package/dist/core/commands/resource-command.js +100 -3
- package/dist/core/commands/variables-command.d.ts +7 -0
- package/dist/core/commands/variables-command.js +13 -0
- package/dist/core/dependencies/env-dependencies-command.js +6 -3
- package/dist/core/executors/javascript-execution-task.js +36 -5
- package/dist/core/types.d.ts +59 -9
- package/dist/core/types.js +64 -10
- package/dist/core/utils/dependencies.js +4 -1
- package/dist/env.js +1 -1
- package/dist/repositories/process.d.ts +8 -2
- package/dist/repositories/process.js +29 -15
- package/dist/repositories/programming-language-manifest.d.ts +31 -1
- package/dist/repositories/programming-language-manifest.js +96 -2
- package/dist/repositories/settings.d.ts +52 -0
- package/dist/repositories/settings.js +214 -0
- package/dist/runtime/libs/python/fcode/context.py +42 -8
- package/dist/services/dashboard-client.d.ts +31 -0
- package/dist/services/dashboard-client.js +99 -0
- package/dist/services/dev-team-service.d.ts +41 -0
- package/dist/services/dev-team-service.js +162 -0
- package/dist/services/fcode-graphql-client.d.ts +17 -2
- package/dist/services/fcode-graphql-client.js +127 -15
- package/dist/services/locale-resolver.d.ts +17 -1
- package/dist/services/locale-resolver.js +62 -7
- package/dist/services/resource-differ.d.ts +60 -0
- package/dist/services/resource-differ.js +135 -0
- package/dist/services/skills-installer.d.ts +16 -0
- package/dist/services/skills-installer.js +77 -0
- package/dist/services/team-versioning.d.ts +16 -0
- package/dist/services/team-versioning.js +40 -0
- package/dist/services/webhook-auth.js +1 -1
- package/dist/services/workspace-cloner.d.ts +31 -0
- package/dist/services/workspace-cloner.js +95 -0
- package/dist/utils/dev-slug.d.ts +12 -0
- package/dist/utils/dev-slug.js +37 -0
- package/dist/utils/diff.d.ts +19 -0
- package/dist/utils/diff.js +58 -0
- package/dist/utils/gitignore.d.ts +3 -1
- package/dist/utils/gitignore.js +11 -2
- package/dist/utils/slugify.d.ts +17 -0
- package/dist/utils/slugify.js +41 -0
- package/oclif.manifest.json +433 -113
- package/package.json +12 -7
- package/dist/commands/team/push.js +0 -52
- package/dist/commands/team/versions/create.js +0 -42
- package/dist/repositories/team.d.ts +0 -31
- package/dist/repositories/team.js +0 -127
|
@@ -12,7 +12,10 @@ async function getDependenciesPath(programmingLanguage) {
|
|
|
12
12
|
const basePath = (0, workspace_1.baseDependenciesPath)(programmingLanguage);
|
|
13
13
|
const repo = new programming_language_manifest_1.default();
|
|
14
14
|
const programmingLanguageManifest = await repo.find(programmingLanguage);
|
|
15
|
-
|
|
15
|
+
// The effective set, not the workspace's own: the cloud installs the inherited dependencies too,
|
|
16
|
+
// and the directory is named after the hash of what is installed. Hashing only the own list here
|
|
17
|
+
// would make a local run resolve a different directory than the platform.
|
|
18
|
+
const dependenciesDirectory = (0, env_dependencies_command_1.dependenciesHash)(programmingLanguageManifest.effectiveDependencies);
|
|
16
19
|
return path.join(basePath, dependenciesDirectory);
|
|
17
20
|
}
|
|
18
21
|
function getProcessDependenciesPath(process) {
|
package/dist/env.js
CHANGED
|
@@ -14,7 +14,7 @@ const zod_1 = require("zod");
|
|
|
14
14
|
*/
|
|
15
15
|
exports.envSchema = zod_1.z.object({
|
|
16
16
|
// --- Cloud / auth endpoints ---
|
|
17
|
-
FCODE_CLOUD_URL: zod_1.z.string().min(1).default('https://code.
|
|
17
|
+
FCODE_CLOUD_URL: zod_1.z.string().min(1).default('https://code.factorialhr.com/platform'),
|
|
18
18
|
FCODE_AUTH_URL: zod_1.z.string().min(1).optional(),
|
|
19
19
|
FCODE_CLIENT_ID: zod_1.z.string().min(1).default('cli'),
|
|
20
20
|
// --- Local config location (defaults to os.homedir() at the call site) ---
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { Process, ResourceMetadata, VersionedProcess } from '../core/types';
|
|
1
|
+
import { Process, ProcessMetadataFields, ProgrammingLanguage, ResourceMetadata, VersionedProcess } from '../core/types';
|
|
2
2
|
import ResourceRepositoryBase from './resource';
|
|
3
|
+
/**
|
|
4
|
+
* What a process writes to metadata.json. Exported so anything rendering a process as its files
|
|
5
|
+
* — `saveFiles` here, `fcode diff` — produces the same bytes for the same process.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildProcessMetadata(process: Process): ProcessMetadataFields;
|
|
8
|
+
export declare const PROCESS_SOURCE_CODE_FILE_NAME: (programmingLanguage: string) => string;
|
|
9
|
+
export declare const PROCESS_DEPENDENCIES_FILE_NAME: (programmingLanguage: ProgrammingLanguage) => string;
|
|
3
10
|
declare class ProcessRepository extends ResourceRepositoryBase<Process> {
|
|
4
11
|
constructor();
|
|
5
12
|
protected buildResourcePath(process: Process, filename: string, version?: string): string;
|
|
@@ -12,7 +19,6 @@ declare class ProcessRepository extends ResourceRepositoryBase<Process> {
|
|
|
12
19
|
findSlugsNotAdded(): Promise<Array<string>>;
|
|
13
20
|
find(processSlug: string, version?: string): Promise<Process>;
|
|
14
21
|
private readMetadataFile;
|
|
15
|
-
private buildMetadata;
|
|
16
22
|
findParameters(processSlug: string): Promise<any>;
|
|
17
23
|
getLastFileModifiedDate(process: Process, filename: string): Promise<Date>;
|
|
18
24
|
protected getResourcePath(process: Process): string;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROCESS_DEPENDENCIES_FILE_NAME = exports.PROCESS_SOURCE_CODE_FILE_NAME = void 0;
|
|
4
|
+
exports.buildProcessMetadata = buildProcessMetadata;
|
|
3
5
|
const fs = require("fs-extra");
|
|
4
6
|
const crypto = require("node:crypto");
|
|
5
7
|
const path = require("node:path");
|
|
@@ -17,6 +19,30 @@ const PROGRAMMING_LANGUAGE_DEPENDENCIES_FILE_NAME = {
|
|
|
17
19
|
JAVASCRIPT: 'package.json',
|
|
18
20
|
PYTHON: 'requirements.txt',
|
|
19
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* What a process writes to metadata.json. Exported so anything rendering a process as its files
|
|
24
|
+
* — `saveFiles` here, `fcode diff` — produces the same bytes for the same process.
|
|
25
|
+
*/
|
|
26
|
+
function buildProcessMetadata(process) {
|
|
27
|
+
const metadata = { name: process.name, tags: process.tags ?? [] };
|
|
28
|
+
if (process.description !== undefined) {
|
|
29
|
+
metadata.description = process.description;
|
|
30
|
+
}
|
|
31
|
+
if (process.webhook !== undefined) {
|
|
32
|
+
metadata.webhook = process.webhook;
|
|
33
|
+
}
|
|
34
|
+
if (process.form !== undefined) {
|
|
35
|
+
metadata.form = process.form;
|
|
36
|
+
}
|
|
37
|
+
if (process.uiTrigger !== undefined) {
|
|
38
|
+
metadata.uiTrigger = process.uiTrigger;
|
|
39
|
+
}
|
|
40
|
+
return metadata;
|
|
41
|
+
}
|
|
42
|
+
const PROCESS_SOURCE_CODE_FILE_NAME = (programmingLanguage) => (programmingLanguage === 'PYTHON' ? 'main.py' : 'index.js');
|
|
43
|
+
exports.PROCESS_SOURCE_CODE_FILE_NAME = PROCESS_SOURCE_CODE_FILE_NAME;
|
|
44
|
+
const PROCESS_DEPENDENCIES_FILE_NAME = (programmingLanguage) => PROGRAMMING_LANGUAGE_DEPENDENCIES_FILE_NAME[programmingLanguage];
|
|
45
|
+
exports.PROCESS_DEPENDENCIES_FILE_NAME = PROCESS_DEPENDENCIES_FILE_NAME;
|
|
20
46
|
class ProcessRepository extends resource_1.default {
|
|
21
47
|
constructor() {
|
|
22
48
|
super({
|
|
@@ -34,7 +60,7 @@ class ProcessRepository extends resource_1.default {
|
|
|
34
60
|
return (0, filenamify_1.default)(process.slug);
|
|
35
61
|
}
|
|
36
62
|
getSourceCodeFileName(programmingLanguage) {
|
|
37
|
-
return
|
|
63
|
+
return (0, exports.PROCESS_SOURCE_CODE_FILE_NAME)(programmingLanguage);
|
|
38
64
|
}
|
|
39
65
|
async findAll() {
|
|
40
66
|
const { processes } = await this.workspaceConfig.getActiveTeam();
|
|
@@ -136,6 +162,7 @@ class ProcessRepository extends resource_1.default {
|
|
|
136
162
|
tags: Array.isArray(raw.tags) ? raw.tags : [],
|
|
137
163
|
webhook: raw.webhook,
|
|
138
164
|
form: raw.form,
|
|
165
|
+
uiTrigger: raw.uiTrigger,
|
|
139
166
|
};
|
|
140
167
|
}
|
|
141
168
|
catch (error) {
|
|
@@ -145,19 +172,6 @@ class ProcessRepository extends resource_1.default {
|
|
|
145
172
|
return { name: processSlug, tags: [] };
|
|
146
173
|
}
|
|
147
174
|
}
|
|
148
|
-
buildMetadata(process) {
|
|
149
|
-
const metadata = { name: process.name, tags: process.tags ?? [] };
|
|
150
|
-
if (process.description !== undefined) {
|
|
151
|
-
metadata.description = process.description;
|
|
152
|
-
}
|
|
153
|
-
if (process.webhook !== undefined) {
|
|
154
|
-
metadata.webhook = process.webhook;
|
|
155
|
-
}
|
|
156
|
-
if (process.form !== undefined) {
|
|
157
|
-
metadata.form = process.form;
|
|
158
|
-
}
|
|
159
|
-
return metadata;
|
|
160
|
-
}
|
|
161
175
|
async findParameters(processSlug) {
|
|
162
176
|
const processMetadata = await this.getProcessMetadata(processSlug);
|
|
163
177
|
if (!processMetadata) {
|
|
@@ -234,7 +248,7 @@ class ProcessRepository extends resource_1.default {
|
|
|
234
248
|
readOnly,
|
|
235
249
|
});
|
|
236
250
|
(0, file_utils_1.outputFileSyncIfChanged)(this.buildResourcePath(process, 'README.md'), process.readme || '', { readOnly });
|
|
237
|
-
(0, file_utils_1.outputFileSyncIfChanged)(this.buildResourcePath(process, 'metadata.json'), JSON.stringify(
|
|
251
|
+
(0, file_utils_1.outputFileSyncIfChanged)(this.buildResourcePath(process, 'metadata.json'), JSON.stringify(buildProcessMetadata(process), null, 2), { readOnly });
|
|
238
252
|
(0, file_utils_1.outputFileSyncIfChanged)(this.buildResourcePath(process, 'parametersSchema.json'), JSON.stringify(process.script.parametersSchema, null, 2), { readOnly });
|
|
239
253
|
const parametersFilePath = this.buildResourcePath(process, 'parameters.json');
|
|
240
254
|
if (!fs.existsSync(parametersFilePath)) {
|
|
@@ -1,17 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProgrammingLanguage, ProgrammingLanguageManifest, ResourceMetadata, VersionedProgrammingLanguageManifest } from '../core/types';
|
|
2
2
|
import ResourceRepositoryBase from './resource';
|
|
3
|
+
/**
|
|
4
|
+
* The inherited file each language collapses its parents into. Exported so the gitignore sync names
|
|
5
|
+
* exactly what this repository writes, rather than repeating the strings.
|
|
6
|
+
*/
|
|
7
|
+
export declare const INHERITED_FILE_NAME: Record<ProgrammingLanguage, string>;
|
|
8
|
+
export declare const DEPENDENCIES_DIR = "dependencies";
|
|
3
9
|
declare class ProgrammingLanguageManifestRepository extends ResourceRepositoryBase<ProgrammingLanguageManifest> {
|
|
4
10
|
constructor();
|
|
5
11
|
find(slug: string): Promise<ProgrammingLanguageManifest>;
|
|
6
12
|
protected getSourceCodeFileName(programmingLanguageManifest: ProgrammingLanguageManifest): string;
|
|
13
|
+
private getSourceCodeFileNameFor;
|
|
14
|
+
/**
|
|
15
|
+
* The inherited dependencies of one language, collapsed into a single file the way inherited
|
|
16
|
+
* variables are (`variables.inherited.env`) rather than one file per parent: two versions of the
|
|
17
|
+
* same package cannot be installed side by side, so there is only ever one winner per name and
|
|
18
|
+
* the platform has already picked it.
|
|
19
|
+
*/
|
|
20
|
+
protected getInheritedFileName(programmingLanguage: ProgrammingLanguage): string;
|
|
21
|
+
getInheritedPath(programmingLanguage: ProgrammingLanguage): string;
|
|
22
|
+
private readInheritedDependencies;
|
|
7
23
|
protected getResourceDirectory(programmingLanguageManifest: ProgrammingLanguageManifest): string;
|
|
8
24
|
protected getResourcePath(programmingLanguageManifest: ProgrammingLanguageManifest): string;
|
|
9
25
|
protected removeFiles(programmingLanguageManifest: ProgrammingLanguageManifest): Promise<void>;
|
|
10
26
|
protected resourceToMetadata(programmingLanguageManifest: ProgrammingLanguageManifest): ResourceMetadata;
|
|
11
27
|
protected saveFiles(programmingLanguageManifest: ProgrammingLanguageManifest): Promise<void>;
|
|
28
|
+
private removeInheritedFile;
|
|
12
29
|
metadataFromFiles(slug: string): Promise<ResourceMetadata>;
|
|
13
30
|
validSlug(slug: string): boolean;
|
|
14
31
|
slugFormatMessage(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Published snapshots live under `dependencies/versions/<tag>/`, one manifest file per language,
|
|
34
|
+
* mirroring the `versions/` folders of processes and modules. Written read-only: unlike those,
|
|
35
|
+
* these are pulled for visibility only — `fcode settings:versions:create` is what publishes them, and
|
|
36
|
+
* editing one here would push nothing.
|
|
37
|
+
*/
|
|
38
|
+
private versionsDirectory;
|
|
39
|
+
private versionDirectory;
|
|
40
|
+
private versionPath;
|
|
41
|
+
getLocalVersionTags(): string[];
|
|
42
|
+
versionExistsLocally(version: VersionedProgrammingLanguageManifest): boolean;
|
|
43
|
+
removeVersion(tag: string): void;
|
|
44
|
+
saveVersion(version: VersionedProgrammingLanguageManifest): Promise<void>;
|
|
15
45
|
findSlugsNotAdded(): Promise<Array<string>>;
|
|
16
46
|
}
|
|
17
47
|
export default ProgrammingLanguageManifestRepository;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEPENDENCIES_DIR = exports.INHERITED_FILE_NAME = void 0;
|
|
3
4
|
const fs = require("fs-extra");
|
|
4
5
|
const path = require("node:path");
|
|
5
6
|
const errors_1 = require("../core/errors");
|
|
@@ -7,6 +8,16 @@ const types_1 = require("../core/types");
|
|
|
7
8
|
const resource_1 = require("./resource");
|
|
8
9
|
const dependencies_1 = require("../core/utils/dependencies");
|
|
9
10
|
const file_utils_1 = require("../utils/file-utils");
|
|
11
|
+
/**
|
|
12
|
+
* The inherited file each language collapses its parents into. Exported so the gitignore sync names
|
|
13
|
+
* exactly what this repository writes, rather than repeating the strings.
|
|
14
|
+
*/
|
|
15
|
+
exports.INHERITED_FILE_NAME = {
|
|
16
|
+
JAVASCRIPT: 'package.inherited.json',
|
|
17
|
+
PYTHON: 'requirements.inherited.txt',
|
|
18
|
+
};
|
|
19
|
+
exports.DEPENDENCIES_DIR = 'dependencies';
|
|
20
|
+
const VERSIONS_DIR = 'versions';
|
|
10
21
|
class ProgrammingLanguageManifestRepository extends resource_1.default {
|
|
11
22
|
constructor() {
|
|
12
23
|
super({
|
|
@@ -21,14 +32,36 @@ class ProgrammingLanguageManifestRepository extends resource_1.default {
|
|
|
21
32
|
}
|
|
22
33
|
const sourceCodeFileContent = fs.readFileSync(this.getResourcePath(programmingLanguageManifest));
|
|
23
34
|
try {
|
|
24
|
-
return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.slug, programmingLanguageManifest.programmingLanguage, (0, dependencies_1.dependenciesFromFile)(programmingLanguageManifest.programmingLanguage, sourceCodeFileContent.toString()));
|
|
35
|
+
return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.slug, programmingLanguageManifest.programmingLanguage, (0, dependencies_1.dependenciesFromFile)(programmingLanguageManifest.programmingLanguage, sourceCodeFileContent.toString()), this.readInheritedDependencies(programmingLanguageManifest.programmingLanguage));
|
|
25
36
|
}
|
|
26
37
|
catch (error) {
|
|
27
38
|
throw new errors_1.LocalProgrammingLanguageManifestNotFound(error, slug);
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
41
|
getSourceCodeFileName(programmingLanguageManifest) {
|
|
31
|
-
return programmingLanguageManifest.programmingLanguage
|
|
42
|
+
return this.getSourceCodeFileNameFor(programmingLanguageManifest.programmingLanguage);
|
|
43
|
+
}
|
|
44
|
+
getSourceCodeFileNameFor(programmingLanguage) {
|
|
45
|
+
return programmingLanguage === 'JAVASCRIPT' ? 'package.json' : 'requirements.txt';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The inherited dependencies of one language, collapsed into a single file the way inherited
|
|
49
|
+
* variables are (`variables.inherited.env`) rather than one file per parent: two versions of the
|
|
50
|
+
* same package cannot be installed side by side, so there is only ever one winner per name and
|
|
51
|
+
* the platform has already picked it.
|
|
52
|
+
*/
|
|
53
|
+
getInheritedFileName(programmingLanguage) {
|
|
54
|
+
return exports.INHERITED_FILE_NAME[programmingLanguage];
|
|
55
|
+
}
|
|
56
|
+
getInheritedPath(programmingLanguage) {
|
|
57
|
+
return path.join(this._repositoryName, this.getInheritedFileName(programmingLanguage));
|
|
58
|
+
}
|
|
59
|
+
readInheritedDependencies(programmingLanguage) {
|
|
60
|
+
const inheritedPath = this.getInheritedPath(programmingLanguage);
|
|
61
|
+
if (!fs.existsSync(inheritedPath)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return (0, dependencies_1.dependenciesFromFile)(programmingLanguage, fs.readFileSync(inheritedPath).toString());
|
|
32
65
|
}
|
|
33
66
|
getResourceDirectory(programmingLanguageManifest) {
|
|
34
67
|
return this._repositoryName;
|
|
@@ -40,6 +73,7 @@ class ProgrammingLanguageManifestRepository extends resource_1.default {
|
|
|
40
73
|
await new Promise((resolve, reject) => {
|
|
41
74
|
try {
|
|
42
75
|
fs.removeSync(this.getResourcePath(programmingLanguageManifest));
|
|
76
|
+
this.removeInheritedFile(programmingLanguageManifest.programmingLanguage);
|
|
43
77
|
this.removeDirectoryIfEmpty(this.getResourceDirectory(programmingLanguageManifest));
|
|
44
78
|
resolve(true);
|
|
45
79
|
}
|
|
@@ -57,6 +91,26 @@ class ProgrammingLanguageManifestRepository extends resource_1.default {
|
|
|
57
91
|
}
|
|
58
92
|
async saveFiles(programmingLanguageManifest) {
|
|
59
93
|
(0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(programmingLanguageManifest), (0, dependencies_1.dependenciesToFile)(programmingLanguageManifest.programmingLanguage, programmingLanguageManifest.dependencies));
|
|
94
|
+
const { programmingLanguage, inheritedDependencies } = programmingLanguageManifest;
|
|
95
|
+
if (inheritedDependencies.length === 0) {
|
|
96
|
+
// The workspace stopped inheriting anything (a parent was removed, or emptied its manifest):
|
|
97
|
+
// a stale file would keep being installed locally and keep showing in `status`.
|
|
98
|
+
this.removeInheritedFile(programmingLanguage);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Read-only, like inherited locales and modules: these belong to a parent workspace, and
|
|
102
|
+
// editing one here would change nothing — declare the name in package.json to override it.
|
|
103
|
+
(0, file_utils_1.outputFileSyncIfChanged)(this.getInheritedPath(programmingLanguage), (0, dependencies_1.dependenciesToFile)(programmingLanguage, inheritedDependencies), {
|
|
104
|
+
readOnly: true,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
removeInheritedFile(programmingLanguage) {
|
|
108
|
+
const inheritedPath = this.getInheritedPath(programmingLanguage);
|
|
109
|
+
if (fs.existsSync(inheritedPath)) {
|
|
110
|
+
// Written read-only, so it has to be made writable before it can go.
|
|
111
|
+
fs.chmodSync(inheritedPath, 0o644);
|
|
112
|
+
fs.removeSync(inheritedPath);
|
|
113
|
+
}
|
|
60
114
|
}
|
|
61
115
|
async metadataFromFiles(slug) {
|
|
62
116
|
if (slug === 'JAVASCRIPT') {
|
|
@@ -81,6 +135,46 @@ class ProgrammingLanguageManifestRepository extends resource_1.default {
|
|
|
81
135
|
slugFormatMessage() {
|
|
82
136
|
return 'Only JAVASCRIPT or PYTHON are allowed.';
|
|
83
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Published snapshots live under `dependencies/versions/<tag>/`, one manifest file per language,
|
|
140
|
+
* mirroring the `versions/` folders of processes and modules. Written read-only: unlike those,
|
|
141
|
+
* these are pulled for visibility only — `fcode settings:versions:create` is what publishes them, and
|
|
142
|
+
* editing one here would push nothing.
|
|
143
|
+
*/
|
|
144
|
+
versionsDirectory() {
|
|
145
|
+
return path.join(this._repositoryName, VERSIONS_DIR);
|
|
146
|
+
}
|
|
147
|
+
versionDirectory(tag) {
|
|
148
|
+
return path.join(this.versionsDirectory(), tag);
|
|
149
|
+
}
|
|
150
|
+
versionPath(version) {
|
|
151
|
+
return path.join(this.versionDirectory(version.tag), this.getSourceCodeFileNameFor(version.programmingLanguage));
|
|
152
|
+
}
|
|
153
|
+
getLocalVersionTags() {
|
|
154
|
+
const versionsDir = this.versionsDirectory();
|
|
155
|
+
if (!fs.pathExistsSync(versionsDir)) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
return fs.readdirSync(versionsDir).filter((name) => this.validVersion(name));
|
|
159
|
+
}
|
|
160
|
+
versionExistsLocally(version) {
|
|
161
|
+
return fs.pathExistsSync(this.versionPath(version));
|
|
162
|
+
}
|
|
163
|
+
removeVersion(tag) {
|
|
164
|
+
const versionDirectory = this.versionDirectory(tag);
|
|
165
|
+
if (!fs.pathExistsSync(versionDirectory)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Written read-only, so every file has to be made writable before the directory can go.
|
|
169
|
+
for (const fileName of fs.readdirSync(versionDirectory)) {
|
|
170
|
+
fs.chmodSync(path.join(versionDirectory, fileName), 0o644);
|
|
171
|
+
}
|
|
172
|
+
fs.removeSync(versionDirectory);
|
|
173
|
+
this.removeDirectoryIfEmpty(this.versionsDirectory());
|
|
174
|
+
}
|
|
175
|
+
async saveVersion(version) {
|
|
176
|
+
(0, file_utils_1.outputFileSyncIfChanged)(this.versionPath(version), (0, dependencies_1.dependenciesToFile)(version.programmingLanguage, version.dependencies), { readOnly: true });
|
|
177
|
+
}
|
|
84
178
|
async findSlugsNotAdded() {
|
|
85
179
|
const slugsNotAdded = [];
|
|
86
180
|
if (!(await this.workspaceConfig.exists('dependencies', 'JAVASCRIPT')) && fs.existsSync(path.join(this._repositoryName, 'package.json'))) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { RawTeamSettings, TeamVersion, TeamVersionAlias, WorkspaceSettings } from '../core/types';
|
|
2
|
+
import WorkspaceConfig from '../config/workspace';
|
|
3
|
+
export declare const SETTINGS_FILENAME = "settings.json";
|
|
4
|
+
/**
|
|
5
|
+
* Workspaces cloned before the file was renamed carry team.json. Renaming it on first access keeps
|
|
6
|
+
* the stored sync hash valid (it is computed over the content, not the name), so the workspace does
|
|
7
|
+
* not report a phantom conflict. Only fires when the new name is absent, so a workspace holding both
|
|
8
|
+
* is left alone.
|
|
9
|
+
*/
|
|
10
|
+
export declare function migrateLegacySettingsFile(workspaceDir?: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Reads/writes the singleton settings.json (timezone, error handler, inheritance, webhook auth) at
|
|
13
|
+
* the workspace root. Unlike collection resources this is not a ResourceRepositoryBase:
|
|
14
|
+
* a workspace has exactly one settings file, so there is no slug-keyed collection to manage.
|
|
15
|
+
*
|
|
16
|
+
* settings.json references the error-handler process by slug for portability; the cloud
|
|
17
|
+
* references it by id, so this repository resolves slug<->id against the tracked
|
|
18
|
+
* process metadata in .fcode/.
|
|
19
|
+
*/
|
|
20
|
+
export default class SettingsRepository {
|
|
21
|
+
protected workspaceConfig: WorkspaceConfig;
|
|
22
|
+
exists(): boolean;
|
|
23
|
+
find(): Promise<WorkspaceSettings>;
|
|
24
|
+
save(settings: WorkspaceSettings): void;
|
|
25
|
+
/** Builds the settings from the cloud's raw payload, resolving the error-handler id to a slug. */
|
|
26
|
+
fromRaw(raw: RawTeamSettings): Promise<WorkspaceSettings>;
|
|
27
|
+
/**
|
|
28
|
+
* Writes the cloud's workspace versions and aliases into the settings as the settings.json
|
|
29
|
+
* versioning fields (sorted for stable git diffs). Cloud entries always carry a createdAt; only
|
|
30
|
+
* hand-added local entries lack one, and those never pass through here.
|
|
31
|
+
*/
|
|
32
|
+
applyCloudVersioning(settings: WorkspaceSettings, versions: TeamVersion[], aliases: TeamVersionAlias[]): void;
|
|
33
|
+
resolveSlugToId(processSlug: string): Promise<string>;
|
|
34
|
+
resolveIdToSlug(processId: string): Promise<string>;
|
|
35
|
+
private parseErrorHandler;
|
|
36
|
+
/**
|
|
37
|
+
* settings.json is hand-edited to declare versions, so entries without a tag are dropped and a
|
|
38
|
+
* duplicated tag keeps its first entry (a tag names one immutable snapshot).
|
|
39
|
+
*/
|
|
40
|
+
private parseVersions;
|
|
41
|
+
/**
|
|
42
|
+
* An alias is a name->tag pointer; entries missing either half are dropped and a duplicated
|
|
43
|
+
* name keeps its first entry.
|
|
44
|
+
*/
|
|
45
|
+
private parseAliases;
|
|
46
|
+
/** A configuration without a variable key is meaningless, so it is read as absent. */
|
|
47
|
+
private parseWebhookAuth;
|
|
48
|
+
/** Accepts plain slugs (unpinned) next to { slug, version } objects, in file order. */
|
|
49
|
+
private parseParentTeams;
|
|
50
|
+
/** The settings.json shape. Public so `fcode diff` renders cloud settings into the same bytes a pull would write. */
|
|
51
|
+
toFile(settings: WorkspaceSettings): Record<string, any>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SETTINGS_FILENAME = void 0;
|
|
4
|
+
exports.migrateLegacySettingsFile = migrateLegacySettingsFile;
|
|
5
|
+
const fs = require("fs-extra");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const types_1 = require("../core/types");
|
|
8
|
+
const workspace_1 = require("../config/workspace");
|
|
9
|
+
const file_utils_1 = require("../utils/file-utils");
|
|
10
|
+
exports.SETTINGS_FILENAME = 'settings.json';
|
|
11
|
+
const LEGACY_SETTINGS_FILENAME = 'team.json';
|
|
12
|
+
/**
|
|
13
|
+
* Workspaces cloned before the file was renamed carry team.json. Renaming it on first access keeps
|
|
14
|
+
* the stored sync hash valid (it is computed over the content, not the name), so the workspace does
|
|
15
|
+
* not report a phantom conflict. Only fires when the new name is absent, so a workspace holding both
|
|
16
|
+
* is left alone.
|
|
17
|
+
*/
|
|
18
|
+
function migrateLegacySettingsFile(workspaceDir = process.cwd()) {
|
|
19
|
+
const legacy = path.join(workspaceDir, LEGACY_SETTINGS_FILENAME);
|
|
20
|
+
const current = path.join(workspaceDir, exports.SETTINGS_FILENAME);
|
|
21
|
+
if (fs.pathExistsSync(legacy) && !fs.pathExistsSync(current)) {
|
|
22
|
+
fs.renameSync(legacy, current);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads/writes the singleton settings.json (timezone, error handler, inheritance, webhook auth) at
|
|
27
|
+
* the workspace root. Unlike collection resources this is not a ResourceRepositoryBase:
|
|
28
|
+
* a workspace has exactly one settings file, so there is no slug-keyed collection to manage.
|
|
29
|
+
*
|
|
30
|
+
* settings.json references the error-handler process by slug for portability; the cloud
|
|
31
|
+
* references it by id, so this repository resolves slug<->id against the tracked
|
|
32
|
+
* process metadata in .fcode/.
|
|
33
|
+
*/
|
|
34
|
+
class SettingsRepository {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.workspaceConfig = new workspace_1.default();
|
|
37
|
+
}
|
|
38
|
+
exists() {
|
|
39
|
+
migrateLegacySettingsFile();
|
|
40
|
+
return fs.pathExistsSync(exports.SETTINGS_FILENAME);
|
|
41
|
+
}
|
|
42
|
+
async find() {
|
|
43
|
+
migrateLegacySettingsFile();
|
|
44
|
+
const slug = await this.workspaceConfig.getActiveTeamSlug();
|
|
45
|
+
let raw = {};
|
|
46
|
+
try {
|
|
47
|
+
raw = fs.readJsonSync(exports.SETTINGS_FILENAME) || {};
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
const settings = new types_1.WorkspaceSettings(slug, this.parseParentTeams(raw.parentTeams), raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig), this.parseWebhookAuth(raw.webhookAuth), typeof raw.primaryLocale === 'string' && raw.primaryLocale.length > 0 ? raw.primaryLocale : undefined);
|
|
51
|
+
if (Array.isArray(raw.versions)) {
|
|
52
|
+
settings.versions = this.parseVersions(raw.versions);
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(raw.aliases)) {
|
|
55
|
+
settings.aliases = this.parseAliases(raw.aliases);
|
|
56
|
+
}
|
|
57
|
+
return settings;
|
|
58
|
+
}
|
|
59
|
+
save(settings) {
|
|
60
|
+
migrateLegacySettingsFile();
|
|
61
|
+
(0, file_utils_1.outputFileSyncIfChanged)(exports.SETTINGS_FILENAME, JSON.stringify(this.toFile(settings), null, 2));
|
|
62
|
+
}
|
|
63
|
+
/** Builds the settings from the cloud's raw payload, resolving the error-handler id to a slug. */
|
|
64
|
+
async fromRaw(raw) {
|
|
65
|
+
let errorHandlerConfig;
|
|
66
|
+
if (raw.errorHandlerConfig?.processId) {
|
|
67
|
+
errorHandlerConfig = {
|
|
68
|
+
processSlug: await this.resolveIdToSlug(raw.errorHandlerConfig.processId),
|
|
69
|
+
tag: raw.errorHandlerConfig.tag ?? null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return new types_1.WorkspaceSettings(raw.slug, raw.parentTeams ?? [], raw.zoneId, errorHandlerConfig, this.parseWebhookAuth(raw.webhookAuth), raw.primaryLocale ?? undefined);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Writes the cloud's workspace versions and aliases into the settings as the settings.json
|
|
76
|
+
* versioning fields (sorted for stable git diffs). Cloud entries always carry a createdAt; only
|
|
77
|
+
* hand-added local entries lack one, and those never pass through here.
|
|
78
|
+
*/
|
|
79
|
+
applyCloudVersioning(settings, versions, aliases) {
|
|
80
|
+
settings.versions = versions
|
|
81
|
+
.map((version) => {
|
|
82
|
+
const entry = { tag: version.tag, createdAt: version.createdAt };
|
|
83
|
+
if (version.comment) {
|
|
84
|
+
entry.comment = version.comment;
|
|
85
|
+
}
|
|
86
|
+
return entry;
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => (a.createdAt ?? '').localeCompare(b.createdAt ?? ''));
|
|
89
|
+
settings.aliases = aliases
|
|
90
|
+
.map((alias) => ({ name: alias.name, tag: alias.tag }))
|
|
91
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
92
|
+
}
|
|
93
|
+
async resolveSlugToId(processSlug) {
|
|
94
|
+
const { processes } = await this.workspaceConfig.getActiveTeam();
|
|
95
|
+
return processes[processSlug]?.id ?? processSlug;
|
|
96
|
+
}
|
|
97
|
+
async resolveIdToSlug(processId) {
|
|
98
|
+
const { processes } = await this.workspaceConfig.getActiveTeam();
|
|
99
|
+
const match = Object.values(processes).find((process) => process.id === processId);
|
|
100
|
+
return match?.slug ?? processId;
|
|
101
|
+
}
|
|
102
|
+
parseErrorHandler(raw) {
|
|
103
|
+
if (!raw || !raw.processSlug) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return { processSlug: raw.processSlug, tag: raw.tag ?? null };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* settings.json is hand-edited to declare versions, so entries without a tag are dropped and a
|
|
110
|
+
* duplicated tag keeps its first entry (a tag names one immutable snapshot).
|
|
111
|
+
*/
|
|
112
|
+
parseVersions(raw) {
|
|
113
|
+
const versions = [];
|
|
114
|
+
for (const candidate of raw) {
|
|
115
|
+
if (!candidate || typeof candidate.tag !== 'string' || candidate.tag.length === 0) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (versions.some(({ tag }) => tag === candidate.tag)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const entry = { tag: candidate.tag };
|
|
122
|
+
if (typeof candidate.comment === 'string' && candidate.comment.length > 0) {
|
|
123
|
+
entry.comment = candidate.comment;
|
|
124
|
+
}
|
|
125
|
+
if (typeof candidate.createdAt === 'string' && candidate.createdAt.length > 0) {
|
|
126
|
+
entry.createdAt = candidate.createdAt;
|
|
127
|
+
}
|
|
128
|
+
versions.push(entry);
|
|
129
|
+
}
|
|
130
|
+
return versions;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* An alias is a name->tag pointer; entries missing either half are dropped and a duplicated
|
|
134
|
+
* name keeps its first entry.
|
|
135
|
+
*/
|
|
136
|
+
parseAliases(raw) {
|
|
137
|
+
const aliases = [];
|
|
138
|
+
for (const candidate of raw) {
|
|
139
|
+
if (!candidate || typeof candidate.name !== 'string' || candidate.name.length === 0) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (typeof candidate.tag !== 'string' || candidate.tag.length === 0) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (aliases.some(({ name }) => name === candidate.name)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
aliases.push({ name: candidate.name, tag: candidate.tag });
|
|
149
|
+
}
|
|
150
|
+
return aliases;
|
|
151
|
+
}
|
|
152
|
+
/** A configuration without a variable key is meaningless, so it is read as absent. */
|
|
153
|
+
parseWebhookAuth(raw) {
|
|
154
|
+
if (!raw || typeof raw.variableKey !== 'string' || raw.variableKey.length === 0) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const webhookAuth = { variableKey: raw.variableKey };
|
|
158
|
+
// The default header is left out so a pull writes the minimal form.
|
|
159
|
+
if (typeof raw.headerName === 'string' && raw.headerName.length > 0 && raw.headerName !== types_1.DEFAULT_WEBHOOK_AUTH_HEADER) {
|
|
160
|
+
webhookAuth.headerName = raw.headerName;
|
|
161
|
+
}
|
|
162
|
+
return webhookAuth;
|
|
163
|
+
}
|
|
164
|
+
/** Accepts plain slugs (unpinned) next to { slug, version } objects, in file order. */
|
|
165
|
+
parseParentTeams(raw) {
|
|
166
|
+
if (!Array.isArray(raw)) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const refs = [];
|
|
170
|
+
for (const entry of raw) {
|
|
171
|
+
if (typeof entry === 'string' && entry.length > 0) {
|
|
172
|
+
refs.push({ slug: entry });
|
|
173
|
+
}
|
|
174
|
+
else if (entry && typeof entry === 'object' && typeof entry.slug === 'string' && entry.slug.length > 0) {
|
|
175
|
+
const ref = { slug: entry.slug };
|
|
176
|
+
if (typeof entry.version === 'string' && entry.version.length > 0) {
|
|
177
|
+
ref.version = entry.version;
|
|
178
|
+
}
|
|
179
|
+
refs.push(ref);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return refs;
|
|
183
|
+
}
|
|
184
|
+
/** The settings.json shape. Public so `fcode diff` renders cloud settings into the same bytes a pull would write. */
|
|
185
|
+
toFile(settings) {
|
|
186
|
+
// Unpinned parents are written as plain slugs (the minimal form, and what a hand-written
|
|
187
|
+
// settings.json most likely contains); pinned ones as { slug, version } objects.
|
|
188
|
+
const file = {
|
|
189
|
+
parentTeams: (settings.parentTeams ?? []).map((ref) => (ref.version ? { slug: ref.slug, version: ref.version } : ref.slug)),
|
|
190
|
+
};
|
|
191
|
+
if (settings.zoneId !== undefined) {
|
|
192
|
+
file.zoneId = settings.zoneId;
|
|
193
|
+
}
|
|
194
|
+
if (settings.errorHandlerConfig !== undefined) {
|
|
195
|
+
file.errorHandlerConfig = settings.errorHandlerConfig;
|
|
196
|
+
}
|
|
197
|
+
// Last, so adding webhook auth to an existing settings.json is a purely additive diff.
|
|
198
|
+
if (settings.webhookAuth !== undefined) {
|
|
199
|
+
file.webhookAuth = settings.webhookAuth;
|
|
200
|
+
}
|
|
201
|
+
if (settings.primaryLocale !== undefined) {
|
|
202
|
+
file.primaryLocale = settings.primaryLocale;
|
|
203
|
+
}
|
|
204
|
+
// Global versioning fields, appended after the scalar settings.
|
|
205
|
+
if (settings.versions !== undefined) {
|
|
206
|
+
file.versions = settings.versions;
|
|
207
|
+
}
|
|
208
|
+
if (settings.aliases !== undefined) {
|
|
209
|
+
file.aliases = settings.aliases;
|
|
210
|
+
}
|
|
211
|
+
return file;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.default = SettingsRepository;
|
|
@@ -81,22 +81,32 @@ class Processes(dict):
|
|
|
81
81
|
class I18n:
|
|
82
82
|
"""Local equivalent of the cloud's fcode.i18n.
|
|
83
83
|
|
|
84
|
-
The CLI has already layered the chosen locale over the workspace's primary one, so
|
|
85
|
-
arrives here is
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
The CLI has already layered the chosen locale over the workspace's primary one, so the base
|
|
85
|
+
map that arrives here is flat. ``byLocale`` carries each locale's own files, so a call naming
|
|
86
|
+
a locale (``{"locale": "es"}``) resolves against that locale first and falls back key by key
|
|
87
|
+
to the base map, exactly like the cloud executors. Nothing raises: a key with no translation
|
|
88
|
+
resolves to the key itself, and a placeholder with no argument is left exactly as written.
|
|
89
|
+
|
|
90
|
+
The version a call pins in the cloud is accepted and ignored: a workspace directory holds no
|
|
91
|
+
published snapshots, so a pinned call resolves against the files on disk -- the same per-file
|
|
92
|
+
fallback the cloud applies to a locale never published at that tag.
|
|
91
93
|
"""
|
|
92
94
|
|
|
93
95
|
def __init__(self, i18n_payload):
|
|
94
96
|
payload = i18n_payload or {}
|
|
95
97
|
self.__translations = payload.get("translations") or {}
|
|
98
|
+
self.__by_locale = payload.get("byLocale") or {}
|
|
96
99
|
self.locale = payload.get("locale")
|
|
97
100
|
|
|
98
101
|
def __call__(self, key, args=None, options=None):
|
|
99
|
-
|
|
102
|
+
locale = options.get("locale") if isinstance(options, dict) else None
|
|
103
|
+
template = None
|
|
104
|
+
for locale_map in self.__locale_chain(str(locale) if locale is not None else None):
|
|
105
|
+
template = locale_map.get(key)
|
|
106
|
+
if template is not None:
|
|
107
|
+
break
|
|
108
|
+
if template is None:
|
|
109
|
+
template = self.__translations.get(key)
|
|
100
110
|
if template is None:
|
|
101
111
|
return key
|
|
102
112
|
if not isinstance(args, dict):
|
|
@@ -111,6 +121,30 @@ class I18n:
|
|
|
111
121
|
# Single pass, so a substituted value containing %{...} is never rescanned.
|
|
112
122
|
return re.sub(r"%\{([^}]*)\}", replace, template)
|
|
113
123
|
|
|
124
|
+
def __locale_chain(self, locale):
|
|
125
|
+
"""The maps a requested locale opens, most specific first.
|
|
126
|
+
|
|
127
|
+
An identifier no locale matches is shortened one segment at a time ("en-gb" reads "en"),
|
|
128
|
+
by name first and ignoring case only as a last resort -- the same chain the cloud
|
|
129
|
+
executors resolve, so a local run answers a regional variant the same way.
|
|
130
|
+
"""
|
|
131
|
+
if locale is None or not self.__by_locale:
|
|
132
|
+
return []
|
|
133
|
+
names = list(self.__by_locale.keys())
|
|
134
|
+
chain = []
|
|
135
|
+
candidate = locale
|
|
136
|
+
while candidate:
|
|
137
|
+
if candidate in names:
|
|
138
|
+
match = candidate
|
|
139
|
+
else:
|
|
140
|
+
folded = sorted(n for n in names if n.lower() == candidate.lower())
|
|
141
|
+
match = folded[0] if folded else None
|
|
142
|
+
if match is not None and match not in chain:
|
|
143
|
+
chain.append(match)
|
|
144
|
+
last_separator = max(candidate.rfind("-"), candidate.rfind("_"))
|
|
145
|
+
candidate = candidate[:last_separator] if last_separator > 0 else None
|
|
146
|
+
return [self.__by_locale[name] for name in chain if self.__by_locale[name]]
|
|
147
|
+
|
|
114
148
|
|
|
115
149
|
class FcodeGlobal:
|
|
116
150
|
def __init__(self, context, execution, env, datastore, storage, schedule, variables, i18n=None):
|