@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.
Files changed (123) hide show
  1. package/README.md +334 -117
  2. package/dist/commands/clone.d.ts +0 -12
  3. package/dist/commands/clone.js +22 -135
  4. package/dist/commands/{settings → config}/get.d.ts +1 -1
  5. package/dist/commands/{settings → config}/get.js +7 -7
  6. package/dist/commands/{settings → config}/remove.d.ts +1 -1
  7. package/dist/commands/{settings → config}/remove.js +6 -6
  8. package/dist/commands/{settings → config}/set.d.ts +1 -1
  9. package/dist/commands/{settings → config}/set.js +8 -8
  10. package/dist/commands/dependencies/diff.d.ts +9 -0
  11. package/dist/commands/dependencies/diff.js +15 -0
  12. package/dist/commands/dependencies/pull.d.ts +9 -0
  13. package/dist/commands/dependencies/pull.js +44 -1
  14. package/dist/commands/dependencies/status.d.ts +11 -0
  15. package/dist/commands/dependencies/status.js +43 -0
  16. package/dist/commands/diff.d.ts +12 -0
  17. package/dist/commands/diff.js +48 -0
  18. package/dist/commands/http.d.ts +1 -1
  19. package/dist/commands/http.js +7 -7
  20. package/dist/commands/i18n/diff.d.ts +9 -0
  21. package/dist/commands/i18n/diff.js +15 -0
  22. package/dist/commands/i18n/status.d.ts +3 -0
  23. package/dist/commands/i18n/status.js +1 -0
  24. package/dist/commands/modules/diff.d.ts +9 -0
  25. package/dist/commands/modules/diff.js +15 -0
  26. package/dist/commands/modules/status.d.ts +3 -0
  27. package/dist/commands/modules/status.js +1 -0
  28. package/dist/commands/processes/diff.d.ts +9 -0
  29. package/dist/commands/processes/diff.js +15 -0
  30. package/dist/commands/processes/status.d.ts +3 -0
  31. package/dist/commands/processes/status.js +1 -0
  32. package/dist/commands/pull.js +3 -3
  33. package/dist/commands/push.js +2 -2
  34. package/dist/commands/{team → settings}/aliases/delete.d.ts +1 -1
  35. package/dist/commands/{team → settings}/aliases/delete.js +6 -6
  36. package/dist/commands/{team → settings}/aliases/list.d.ts +1 -1
  37. package/dist/commands/{team → settings}/aliases/list.js +8 -8
  38. package/dist/commands/{team → settings}/aliases/set.d.ts +1 -1
  39. package/dist/commands/{team → settings}/aliases/set.js +8 -8
  40. package/dist/commands/settings/diff.d.ts +8 -0
  41. package/dist/commands/settings/diff.js +39 -0
  42. package/dist/commands/{team/push.d.ts → settings/pull.d.ts} +3 -3
  43. package/dist/commands/settings/pull.js +41 -0
  44. package/dist/commands/settings/push.d.ts +13 -0
  45. package/dist/commands/settings/push.js +103 -0
  46. package/dist/commands/settings/status.d.ts +8 -0
  47. package/dist/commands/settings/status.js +46 -0
  48. package/dist/commands/{team → settings}/versions/create.d.ts +1 -2
  49. package/dist/commands/settings/versions/create.js +36 -0
  50. package/dist/commands/{team → settings}/versions/delete.d.ts +1 -1
  51. package/dist/commands/{team → settings}/versions/delete.js +10 -10
  52. package/dist/commands/{team → settings}/versions/list.d.ts +1 -1
  53. package/dist/commands/{team → settings}/versions/list.js +8 -8
  54. package/dist/commands/status.d.ts +3 -0
  55. package/dist/commands/status.js +10 -7
  56. package/dist/commands/team/clone.d.ts +14 -0
  57. package/dist/commands/team/clone.js +91 -0
  58. package/dist/commands/team/pull.d.ts +3 -4
  59. package/dist/commands/team/pull.js +25 -19
  60. package/dist/commands/team/status.d.ts +3 -4
  61. package/dist/commands/team/status.js +34 -27
  62. package/dist/commands/variables/diff.d.ts +9 -0
  63. package/dist/commands/variables/diff.js +15 -0
  64. package/dist/commands/variables/status.d.ts +3 -0
  65. package/dist/commands/variables/status.js +1 -0
  66. package/dist/config/dev-team.d.ts +48 -0
  67. package/dist/config/dev-team.js +62 -0
  68. package/dist/config/workspace.d.ts +2 -2
  69. package/dist/config/workspace.js +4 -2
  70. package/dist/core/commands/base-command.js +5 -0
  71. package/dist/core/commands/dependency-command.d.ts +2 -1
  72. package/dist/core/commands/dependency-command.js +3 -0
  73. package/dist/core/commands/i18n-command.js +2 -0
  74. package/dist/core/commands/modules-command.js +2 -0
  75. package/dist/core/commands/processes-command.js +2 -0
  76. package/dist/core/commands/resource-command.d.ts +26 -1
  77. package/dist/core/commands/resource-command.js +100 -3
  78. package/dist/core/commands/variables-command.d.ts +7 -0
  79. package/dist/core/commands/variables-command.js +13 -0
  80. package/dist/core/dependencies/env-dependencies-command.js +6 -3
  81. package/dist/core/executors/javascript-execution-task.js +36 -5
  82. package/dist/core/types.d.ts +59 -9
  83. package/dist/core/types.js +64 -10
  84. package/dist/core/utils/dependencies.js +4 -1
  85. package/dist/env.js +1 -1
  86. package/dist/repositories/process.d.ts +8 -2
  87. package/dist/repositories/process.js +29 -15
  88. package/dist/repositories/programming-language-manifest.d.ts +31 -1
  89. package/dist/repositories/programming-language-manifest.js +96 -2
  90. package/dist/repositories/settings.d.ts +52 -0
  91. package/dist/repositories/settings.js +214 -0
  92. package/dist/runtime/libs/python/fcode/context.py +42 -8
  93. package/dist/services/dashboard-client.d.ts +31 -0
  94. package/dist/services/dashboard-client.js +99 -0
  95. package/dist/services/dev-team-service.d.ts +41 -0
  96. package/dist/services/dev-team-service.js +162 -0
  97. package/dist/services/fcode-graphql-client.d.ts +17 -2
  98. package/dist/services/fcode-graphql-client.js +127 -15
  99. package/dist/services/locale-resolver.d.ts +17 -1
  100. package/dist/services/locale-resolver.js +62 -7
  101. package/dist/services/resource-differ.d.ts +60 -0
  102. package/dist/services/resource-differ.js +135 -0
  103. package/dist/services/skills-installer.d.ts +16 -0
  104. package/dist/services/skills-installer.js +77 -0
  105. package/dist/services/team-versioning.d.ts +16 -0
  106. package/dist/services/team-versioning.js +40 -0
  107. package/dist/services/webhook-auth.js +1 -1
  108. package/dist/services/workspace-cloner.d.ts +31 -0
  109. package/dist/services/workspace-cloner.js +95 -0
  110. package/dist/utils/dev-slug.d.ts +12 -0
  111. package/dist/utils/dev-slug.js +37 -0
  112. package/dist/utils/diff.d.ts +19 -0
  113. package/dist/utils/diff.js +58 -0
  114. package/dist/utils/gitignore.d.ts +3 -1
  115. package/dist/utils/gitignore.js +11 -2
  116. package/dist/utils/slugify.d.ts +17 -0
  117. package/dist/utils/slugify.js +41 -0
  118. package/oclif.manifest.json +433 -113
  119. package/package.json +12 -7
  120. package/dist/commands/team/push.js +0 -52
  121. package/dist/commands/team/versions/create.js +0 -42
  122. package/dist/repositories/team.d.ts +0 -31
  123. 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
- const dependenciesDirectory = (0, env_dependencies_command_1.dependenciesHash)(programmingLanguageManifest.dependencies);
15
+ // The effective set, not the workspace's own: the cloud installs the inherited dependencies too,
16
+ // and the directory is named after the hash of what is installed. Hashing only the own list here
17
+ // would make a local run resolve a different directory than the platform.
18
+ const dependenciesDirectory = (0, env_dependencies_command_1.dependenciesHash)(programmingLanguageManifest.effectiveDependencies);
16
19
  return path.join(basePath, dependenciesDirectory);
17
20
  }
18
21
  function getProcessDependenciesPath(process) {
package/dist/env.js CHANGED
@@ -14,7 +14,7 @@ const zod_1 = require("zod");
14
14
  */
15
15
  exports.envSchema = zod_1.z.object({
16
16
  // --- Cloud / auth endpoints ---
17
- FCODE_CLOUD_URL: zod_1.z.string().min(1).default('https://code.factorial.dev/platform'),
17
+ FCODE_CLOUD_URL: zod_1.z.string().min(1).default('https://code.factorialhr.com/platform'),
18
18
  FCODE_AUTH_URL: zod_1.z.string().min(1).optional(),
19
19
  FCODE_CLIENT_ID: zod_1.z.string().min(1).default('cli'),
20
20
  // --- Local config location (defaults to os.homedir() at the call site) ---
@@ -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 programmingLanguage === 'PYTHON' ? 'main.py' : 'index.js';
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(this.buildMetadata(process), null, 2), { readOnly });
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 { ResourceMetadata, ProgrammingLanguageManifest } from '../core/types';
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 === 'JAVASCRIPT' ? 'package.json' : 'requirements.txt';
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 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.
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
- template = self.__translations.get(key)
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):