@factorialco/fcode-cli 2.0.0 → 2.2.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 (76) hide show
  1. package/README.md +248 -2
  2. package/dist/commands/add.js +3 -1
  3. package/dist/commands/clone.d.ts +1 -0
  4. package/dist/commands/clone.js +5 -0
  5. package/dist/commands/i18n/add.d.ts +9 -0
  6. package/dist/commands/i18n/add.js +13 -0
  7. package/dist/commands/i18n/pull.d.ts +12 -0
  8. package/dist/commands/i18n/pull.js +14 -0
  9. package/dist/commands/i18n/push.d.ts +12 -0
  10. package/dist/commands/i18n/push.js +14 -0
  11. package/dist/commands/i18n/remove.d.ts +9 -0
  12. package/dist/commands/i18n/remove.js +19 -0
  13. package/dist/commands/i18n/reset.d.ts +9 -0
  14. package/dist/commands/i18n/reset.js +13 -0
  15. package/dist/commands/i18n/status.d.ts +11 -0
  16. package/dist/commands/i18n/status.js +42 -0
  17. package/dist/commands/pull.js +5 -3
  18. package/dist/commands/push.js +5 -3
  19. package/dist/commands/reset.js +3 -1
  20. package/dist/commands/run.d.ts +1 -0
  21. package/dist/commands/run.js +5 -2
  22. package/dist/commands/setup-debug.js +2 -2
  23. package/dist/commands/status.js +4 -2
  24. package/dist/commands/team/aliases/delete.d.ts +9 -0
  25. package/dist/commands/team/aliases/delete.js +28 -0
  26. package/dist/commands/team/aliases/list.d.ts +6 -0
  27. package/dist/commands/team/aliases/list.js +23 -0
  28. package/dist/commands/team/aliases/set.d.ts +10 -0
  29. package/dist/commands/team/aliases/set.js +25 -0
  30. package/dist/commands/team/pull.js +8 -1
  31. package/dist/commands/team/push.js +6 -0
  32. package/dist/commands/team/versions/create.d.ts +13 -0
  33. package/dist/commands/team/versions/create.js +42 -0
  34. package/dist/commands/team/versions/delete.d.ts +12 -0
  35. package/dist/commands/team/versions/delete.js +32 -0
  36. package/dist/commands/team/versions/list.d.ts +6 -0
  37. package/dist/commands/team/versions/list.js +25 -0
  38. package/dist/commands/variables/status.js +11 -0
  39. package/dist/config/remote.js +1 -1
  40. package/dist/config/settings.d.ts +1 -0
  41. package/dist/config/settings.js +6 -0
  42. package/dist/config/workspace.js +10 -0
  43. package/dist/core/commands/i18n-command.d.ts +5 -0
  44. package/dist/core/commands/i18n-command.js +16 -0
  45. package/dist/core/errors.d.ts +6 -0
  46. package/dist/core/errors.js +19 -1
  47. package/dist/core/executors/execution-task.js +4 -0
  48. package/dist/core/executors/javascript-execution-task.js +26 -1
  49. package/dist/core/resource-pusher.js +15 -0
  50. package/dist/core/types.d.ts +66 -2
  51. package/dist/core/types.js +45 -3
  52. package/dist/env.d.ts +2 -2
  53. package/dist/env.js +1 -1
  54. package/dist/repositories/locale.d.ts +24 -0
  55. package/dist/repositories/locale.js +102 -0
  56. package/dist/repositories/resource.js +4 -1
  57. package/dist/repositories/team.d.ts +7 -1
  58. package/dist/repositories/team.js +39 -2
  59. package/dist/repositories/variable.d.ts +24 -0
  60. package/dist/repositories/variable.js +70 -22
  61. package/dist/runtime/executor.js +2 -2
  62. package/dist/runtime/executor.py +2 -2
  63. package/dist/runtime/libs/python/fcode/__init__.py +3 -2
  64. package/dist/runtime/libs/python/fcode/context.py +43 -1
  65. package/dist/services/fcode-graphql-client.d.ts +20 -1
  66. package/dist/services/fcode-graphql-client.js +276 -17
  67. package/dist/services/locale-merger.d.ts +23 -0
  68. package/dist/services/locale-merger.js +111 -0
  69. package/dist/services/locale-resolver.d.ts +18 -0
  70. package/dist/services/locale-resolver.js +120 -0
  71. package/dist/services/process-runner.d.ts +1 -0
  72. package/dist/services/process-runner.js +2 -2
  73. package/dist/utils/gitignore.d.ts +2 -0
  74. package/dist/utils/gitignore.js +9 -0
  75. package/oclif.manifest.json +511 -127
  76. package/package.json +11 -2
@@ -20,6 +20,7 @@ class VariableRepository extends resource_1.default {
20
20
  });
21
21
  this.filename = null;
22
22
  this.localFilename = null;
23
+ this.inheritedFilename = null;
23
24
  this.filenameInitPromise = null;
24
25
  }
25
26
  async ensureFilenamesInitialized() {
@@ -30,40 +31,67 @@ class VariableRepository extends resource_1.default {
30
31
  const globalConfig = new global_1.default();
31
32
  this.filename = await globalConfig.getSetting(settings_1.SettingKeys.VARIABLES_FILE_NAME);
32
33
  this.localFilename = await globalConfig.getSetting(settings_1.SettingKeys.LOCAL_VARIABLES_FILE_NAME);
34
+ this.inheritedFilename = await globalConfig.getSetting(settings_1.SettingKeys.INHERITED_VARIABLES_FILE_NAME);
33
35
  })();
34
36
  return this.filenameInitPromise;
35
37
  }
36
38
  async findAll() {
37
39
  await this.ensureFilenamesInitialized();
38
- const rawVariables = this.readVariablesFromFile();
39
- const localOverrideKeys = new Set(Object.keys(this.readVariablesFromFile(true)));
40
+ const rawVariables = this.readVariablesFromFile('own');
41
+ const inheritedRawVariables = this.readVariablesFromFile('inherited');
42
+ const localOverrideKeys = new Set(Object.keys(this.readVariablesFromFile('local')));
40
43
  const meta = this.readVariablesMeta();
41
44
  const variables = [];
42
45
  const { variables: localVariables } = await this.workspaceConfig.getActiveTeam();
43
46
  for (const localVariableKey of Object.keys(localVariables)) {
44
47
  const localVariable = localVariables[localVariableKey];
45
48
  const isSensitive = meta[localVariable.key]?.isSensitive ?? false;
46
- const variable = types_1.Variable.validate(new types_1.Variable(localVariable.id, localVariable.key, rawVariables[localVariable.key], isSensitive));
49
+ // Writing an inherited key into variables.env is how you override it, so from that
50
+ // moment the variable is this workspace's: it drops the parent's id and inherited
51
+ // marker, which is what makes `push` create it here instead of skipping it.
52
+ const overridesInherited = this.overridesInherited(localVariable, rawVariables);
53
+ const resolvingTeamSlug = overridesInherited ? undefined : localVariable.resolvingTeamSlug;
54
+ const id = overridesInherited ? undefined : localVariable.id;
55
+ const rawValue = resolvingTeamSlug ? inheritedRawVariables[localVariable.key] : rawVariables[localVariable.key];
56
+ const variable = types_1.Variable.validate(new types_1.Variable(id, localVariable.key, rawValue, isSensitive, resolvingTeamSlug));
47
57
  variable.overriddenLocally = localOverrideKeys.has(localVariable.key);
48
58
  variables.push(variable);
49
59
  }
50
60
  return variables;
51
61
  }
62
+ /**
63
+ * Values as a local run sees them. Precedence, lowest first: variables inherited from
64
+ * parent workspaces, this workspace's own variables, and the local overrides file — the
65
+ * same order the platform applies, where a workspace's own value wins over an inherited one.
66
+ * An empty value is not an override but an unset one: the platform rejects blank values, and
67
+ * a variable emptied in variables.env is a removal (see `mapResourceStatus`), so an empty
68
+ * entry falls through to the layer below rather than blanking it.
69
+ */
52
70
  async findAllForExecution() {
53
71
  await this.ensureFilenamesInitialized();
54
- const rawVariables = this.readVariablesFromFile();
55
- const localRawVariables = this.readVariablesFromFile(true);
72
+ const rawVariables = this.readVariablesFromFile('own');
73
+ const inheritedRawVariables = this.readVariablesFromFile('inherited');
74
+ const localRawVariables = this.readVariablesFromFile('local');
56
75
  const meta = this.readVariablesMeta();
57
76
  const variables = [];
58
77
  const { variables: localVariables } = await this.workspaceConfig.getActiveTeam();
59
78
  for (const localVariableKey of Object.keys(localVariables)) {
60
79
  const localVariable = localVariables[localVariableKey];
61
- const value = localRawVariables[localVariable.key] || rawVariables[localVariable.key];
62
- const isSensitive = meta[localVariable.key]?.isSensitive ?? false;
63
- variables.push(types_1.Variable.validate(new types_1.Variable(localVariable.id, localVariable.key, value, isSensitive)));
80
+ const key = localVariable.key;
81
+ const isSensitive = meta[key]?.isSensitive ?? false;
82
+ const value = localRawVariables[key] || rawVariables[key] || inheritedRawVariables[key];
83
+ const resolvingTeamSlug = this.overridesInherited(localVariable, rawVariables) ? undefined : localVariable.resolvingTeamSlug;
84
+ variables.push(types_1.Variable.validate(new types_1.Variable(localVariable.id, key, value, isSensitive, resolvingTeamSlug)));
64
85
  }
65
86
  return variables;
66
87
  }
88
+ /**
89
+ * An inherited key the workspace has given a value of its own, overriding it. An empty entry
90
+ * does not count: the platform has no empty values, so it reads as "not set" here too.
91
+ */
92
+ overridesInherited(metadata, ownRawVariables) {
93
+ return Boolean(metadata.resolvingTeamSlug) && Boolean(ownRawVariables[metadata.key]);
94
+ }
67
95
  async find(slug) {
68
96
  let variables = await this.findAll();
69
97
  variables = variables.filter((v) => v.key === slug);
@@ -72,12 +100,19 @@ class VariableRepository extends resource_1.default {
72
100
  }
73
101
  throw new errors_1.LocalVariableNotFound(new Error('Variable not found on team metadata'), slug);
74
102
  }
103
+ /**
104
+ * Keys declared in the workspace's own variables file that its metadata does not claim yet.
105
+ * A key the workspace inherits counts as one of those: the metadata still points at the
106
+ * parent's variable, and adding it is what turns the entry into this workspace's own.
107
+ */
75
108
  async findSlugsNotAdded() {
76
109
  await this.ensureFilenamesInitialized();
110
+ const ownRawVariables = this.readVariablesFromFile('own');
77
111
  const { variables: localVariables } = await this.workspaceConfig.getActiveTeam();
78
112
  const slugsNotAdded = [];
79
- for (const [key, _value] of Object.entries(this.readVariablesFromFile())) {
80
- if (!localVariables[key]) {
113
+ for (const key of Object.keys(ownRawVariables)) {
114
+ const metadata = localVariables[key];
115
+ if (!metadata || this.overridesInherited(metadata, ownRawVariables)) {
81
116
  slugsNotAdded.push(key);
82
117
  }
83
118
  }
@@ -88,9 +123,10 @@ class VariableRepository extends resource_1.default {
88
123
  }
89
124
  async removeFiles(variable) {
90
125
  await this.ensureFilenamesInitialized();
91
- let rawVariables = this.readVariablesFromFile();
126
+ const file = variable.resolvingTeamSlug ? 'inherited' : 'own';
127
+ const rawVariables = this.readVariablesFromFile(file);
92
128
  delete rawVariables[variable.key];
93
- this.writeRawVariables(rawVariables);
129
+ this.writeRawVariables(rawVariables, file);
94
130
  const meta = this.readVariablesMeta();
95
131
  delete meta[variable.key];
96
132
  this.writeVariablesMeta(meta);
@@ -100,13 +136,20 @@ class VariableRepository extends resource_1.default {
100
136
  id: variable.id,
101
137
  slug: variable.slug,
102
138
  key: variable.key,
139
+ ...(variable.resolvingTeamSlug && { resolvingTeamSlug: variable.resolvingTeamSlug }),
103
140
  };
104
141
  }
142
+ /**
143
+ * A variable owned by this workspace goes to the committed variables file; one inherited
144
+ * from a parent workspace goes to the gitignored inherited file, so a local run can use it
145
+ * without the child workspace's git history carrying a copy of its parent's values.
146
+ */
105
147
  async saveFiles(variable) {
106
148
  await this.ensureFilenamesInitialized();
107
- let rawVariables = this.readVariablesFromFile();
108
- rawVariables = Object.assign(rawVariables, { [variable.key]: this.valueToSave(variable) });
109
- this.writeRawVariables(rawVariables);
149
+ const file = variable.resolvingTeamSlug ? 'inherited' : 'own';
150
+ const rawVariables = this.readVariablesFromFile(file);
151
+ rawVariables[variable.key] = this.valueToSave(variable);
152
+ this.writeRawVariables(rawVariables, file);
110
153
  const meta = this.readVariablesMeta();
111
154
  meta[variable.key] = { isSensitive: variable.isSensitive };
112
155
  this.writeVariablesMeta(meta);
@@ -139,15 +182,20 @@ class VariableRepository extends resource_1.default {
139
182
  }, {});
140
183
  (0, file_utils_1.outputFileSyncIfChanged)(VARIABLES_META_FILENAME, JSON.stringify(sorted, null, 2));
141
184
  }
142
- filePath(useLocalVersion = false) {
143
- if (!this.filename || !this.localFilename) {
185
+ filePath(file = 'own') {
186
+ if (!this.filename || !this.localFilename || !this.inheritedFilename) {
144
187
  throw new Error('Filenames not initialized. Call ensureFilenamesInitialized() first.');
145
188
  }
146
- return path.join(useLocalVersion ? this.localFilename : this.filename);
189
+ const filenames = {
190
+ own: this.filename,
191
+ local: this.localFilename,
192
+ inherited: this.inheritedFilename,
193
+ };
194
+ return path.join(filenames[file]);
147
195
  }
148
- readVariablesFromFile(useLocalVersion = false) {
196
+ readVariablesFromFile(file = 'own') {
149
197
  try {
150
- const result = (0, dotenv_1.parse)(fs.readFileSync(this.filePath(useLocalVersion), 'utf8')) || {};
198
+ const result = (0, dotenv_1.parse)(fs.readFileSync(this.filePath(file), 'utf8')) || {};
151
199
  for (const [key, value] of Object.entries(result)) {
152
200
  result[key] = (0, env_utils_1.unescapeEnvValue)(value);
153
201
  }
@@ -163,12 +211,12 @@ class VariableRepository extends resource_1.default {
163
211
  }
164
212
  return variable.value;
165
213
  }
166
- writeRawVariables(rawVariables) {
214
+ writeRawVariables(rawVariables, file = 'own') {
167
215
  const envContent = Object.entries(rawVariables)
168
216
  .sort((a, b) => a[0].localeCompare(b[0]))
169
217
  .map(([key, value]) => `${key}=${(0, env_utils_1.escapeEnvValue)(value)}`)
170
218
  .join('\n');
171
- (0, file_utils_1.outputFileSyncIfChanged)(this.filePath(), envContent);
219
+ (0, file_utils_1.outputFileSyncIfChanged)(this.filePath(file), envContent);
172
220
  }
173
221
  }
174
222
  exports.default = VariableRepository;
@@ -3,7 +3,7 @@ const path = require('node:path')
3
3
  const { execSync } = require('node:child_process')
4
4
  const os = require('node:os')
5
5
 
6
- const YC_COMMAND = process.env.YC_COMMAND ?? "fcode"
6
+ const FC_COMMAND = process.env.FC_COMMAND ?? "fcode"
7
7
 
8
8
  const file = process.argv[2]
9
9
  if (!fs.existsSync(file)) {
@@ -36,7 +36,7 @@ let dependenciesPath = ''
36
36
  try {
37
37
  version = getVersion(relativePath)
38
38
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fcode-'))
39
- const command = [YC_COMMAND, 'pre-debug', absolutePath, '-t', tempDir]
39
+ const command = [FC_COMMAND, 'pre-debug', absolutePath, '-t', tempDir]
40
40
  if (version) {
41
41
  command.push('-v')
42
42
  command.push(version)
@@ -4,7 +4,7 @@ import subprocess
4
4
  import tempfile
5
5
 
6
6
 
7
- YC_COMMAND = os.environ.get("YC_COMMAND", "fcode")
7
+ FC_COMMAND = os.environ.get("FC_COMMAND", "fcode")
8
8
  file = sys.argv[1]
9
9
  if not os.path.exists(file):
10
10
  print("File not found: " + file)
@@ -31,7 +31,7 @@ main_file_path = ""
31
31
  try:
32
32
  version = get_version(file)
33
33
  tempDir = tempfile.TemporaryDirectory()
34
- command = [YC_COMMAND, "pre-debug", file, "-t", tempDir.name]
34
+ command = [FC_COMMAND, "pre-debug", file, "-t", tempDir.name]
35
35
  if version != None:
36
36
  command.append("-v")
37
37
  command.append(version)
@@ -3,7 +3,7 @@ import json
3
3
  import subprocess
4
4
  import logging
5
5
 
6
- YC_COMMAND = os.environ.get("YC_COMMAND", "fcode")
6
+ FC_COMMAND = os.environ.get("FC_COMMAND", "fcode")
7
7
 
8
8
  metadata = {
9
9
  "execution": {"process": {"name": "", "id": ""}},
@@ -24,7 +24,7 @@ else:
24
24
  os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
25
25
  "[FCODE LIBRARY] Reading metadata from process-metadata command"
26
26
  )
27
- command = [YC_COMMAND, "process-metadata"]
27
+ command = [FC_COMMAND, "process-metadata"]
28
28
  if process_slug:
29
29
  command.append(process_slug)
30
30
  if process_slug and process_version:
@@ -64,6 +64,7 @@ fcode = FcodeGlobal(
64
64
  Storage(),
65
65
  Schedule(),
66
66
  Variables(env_variables),
67
+ metadata.get("i18n", {}),
67
68
  )
68
69
 
69
70
  logging.basicConfig(level=os.environ.get("FCODE_LOG_LEVEL", logging.INFO))
@@ -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)
@@ -1,5 +1,5 @@
1
1
  import { GraphQLClient } from 'graphql-request';
2
- import { Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, Variable, VersionedProcess, VersionedScriptLibrary, ServiceAccount } from '../core/types';
2
+ import { Locale, Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, TeamVersion, TeamVersionAlias, Variable, VersionedProcess, VersionedScriptLibrary, ServiceAccount } from '../core/types';
3
3
  export type FcodeGraphQLClientOptions = {
4
4
  url: string;
5
5
  accessToken: string;
@@ -22,6 +22,16 @@ declare class FcodeGraphQLClient {
22
22
  allVersionedScriptLibraries(scriptLibraryId: string): Promise<Array<VersionedScriptLibrary>>;
23
23
  createVersionedScriptLibrary(versionedScriptLibrary: VersionedScriptLibrary): Promise<void>;
24
24
  allVariables(): Promise<Array<Variable>>;
25
+ /**
26
+ * Every locale the workspace resolves: its own files, plus one merged file per identifier it
27
+ * inherits. A locale the workspace and a parent both define is listed twice by the platform and
28
+ * kept twice here — an override is layered over the parent's file key by key, so the parent's
29
+ * keys are still what the ones it does not mention resolve to.
30
+ */
31
+ allLocales(): Promise<Array<Locale>>;
32
+ pushLocale(locale: Locale): Promise<Locale>;
33
+ updateLocale(locale: Locale): Promise<Locale>;
34
+ removeLocale(locale: string | undefined): Promise<void>;
25
35
  private toProcessInput;
26
36
  pushProcess(process: Process): Promise<Process>;
27
37
  removeProcess(id: String | undefined): Promise<Process>;
@@ -44,6 +54,15 @@ declare class FcodeGraphQLClient {
44
54
  * undefined.
45
55
  */
46
56
  mapTeamSettings(team: any): RawTeamSettings;
57
+ mapTeamVersion(teamVersion: any): TeamVersion;
58
+ mapTeamVersionAlias(alias: any): TeamVersionAlias;
59
+ allTeamVersions(): Promise<Array<TeamVersion>>;
60
+ createTeamVersion(tag: string, comment?: string): Promise<TeamVersion>;
61
+ deleteTeamVersion(tag: string): Promise<TeamVersion>;
62
+ allTeamVersionAliases(): Promise<Array<TeamVersionAlias>>;
63
+ /** The API upserts: an existing alias with the same name is re-pointed to the tag. */
64
+ createTeamVersionAlias(name: string, tag: string): Promise<TeamVersionAlias>;
65
+ deleteTeamVersionAlias(id: string): Promise<TeamVersionAlias>;
47
66
  team(): Promise<RawTeamSettings>;
48
67
  pushTeam(update: TeamUpdateInput): Promise<RawTeamSettings>;
49
68
  mapProgrammingLanguageManifest(programmingLanguageManifest: any): ProgrammingLanguageManifest;