@factorialco/fcode-cli 1.1.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 (234) hide show
  1. package/README.md +1021 -0
  2. package/bin/dev +17 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/run +7 -0
  5. package/bin/run.cmd +3 -0
  6. package/dist/commands/add.d.ts +6 -0
  7. package/dist/commands/add.js +22 -0
  8. package/dist/commands/clone.d.ts +26 -0
  9. package/dist/commands/clone.js +177 -0
  10. package/dist/commands/dependencies/add.d.ts +9 -0
  11. package/dist/commands/dependencies/add.js +13 -0
  12. package/dist/commands/dependencies/install.d.ts +14 -0
  13. package/dist/commands/dependencies/install.js +44 -0
  14. package/dist/commands/dependencies/pull.d.ts +12 -0
  15. package/dist/commands/dependencies/pull.js +14 -0
  16. package/dist/commands/dependencies/push.d.ts +12 -0
  17. package/dist/commands/dependencies/push.js +14 -0
  18. package/dist/commands/dependencies/reset.d.ts +9 -0
  19. package/dist/commands/dependencies/reset.js +13 -0
  20. package/dist/commands/dependencies/status.d.ts +9 -0
  21. package/dist/commands/dependencies/status.js +13 -0
  22. package/dist/commands/http.d.ts +22 -0
  23. package/dist/commands/http.js +272 -0
  24. package/dist/commands/login.d.ts +18 -0
  25. package/dist/commands/login.js +62 -0
  26. package/dist/commands/logout.d.ts +10 -0
  27. package/dist/commands/logout.js +34 -0
  28. package/dist/commands/modules/add.d.ts +9 -0
  29. package/dist/commands/modules/add.js +13 -0
  30. package/dist/commands/modules/pull.d.ts +13 -0
  31. package/dist/commands/modules/pull.js +43 -0
  32. package/dist/commands/modules/push.d.ts +13 -0
  33. package/dist/commands/modules/push.js +35 -0
  34. package/dist/commands/modules/remove.d.ts +9 -0
  35. package/dist/commands/modules/remove.js +19 -0
  36. package/dist/commands/modules/reset.d.ts +9 -0
  37. package/dist/commands/modules/reset.js +13 -0
  38. package/dist/commands/modules/status.d.ts +11 -0
  39. package/dist/commands/modules/status.js +40 -0
  40. package/dist/commands/pre-debug.d.ts +20 -0
  41. package/dist/commands/pre-debug.js +120 -0
  42. package/dist/commands/process-metadata.d.ts +18 -0
  43. package/dist/commands/process-metadata.js +63 -0
  44. package/dist/commands/processes/add.d.ts +9 -0
  45. package/dist/commands/processes/add.js +13 -0
  46. package/dist/commands/processes/install-dependencies.d.ts +14 -0
  47. package/dist/commands/processes/install-dependencies.js +30 -0
  48. package/dist/commands/processes/pull.d.ts +13 -0
  49. package/dist/commands/processes/pull.js +47 -0
  50. package/dist/commands/processes/push.d.ts +13 -0
  51. package/dist/commands/processes/push.js +39 -0
  52. package/dist/commands/processes/remove.d.ts +9 -0
  53. package/dist/commands/processes/remove.js +19 -0
  54. package/dist/commands/processes/reset.d.ts +9 -0
  55. package/dist/commands/processes/reset.js +13 -0
  56. package/dist/commands/processes/status.d.ts +11 -0
  57. package/dist/commands/processes/status.js +58 -0
  58. package/dist/commands/pull.d.ts +9 -0
  59. package/dist/commands/pull.js +32 -0
  60. package/dist/commands/push.d.ts +9 -0
  61. package/dist/commands/push.js +29 -0
  62. package/dist/commands/remote/add.d.ts +10 -0
  63. package/dist/commands/remote/add.js +32 -0
  64. package/dist/commands/remote/list.d.ts +7 -0
  65. package/dist/commands/remote/list.js +33 -0
  66. package/dist/commands/remote/set.d.ts +10 -0
  67. package/dist/commands/remote/set.js +28 -0
  68. package/dist/commands/reset.d.ts +6 -0
  69. package/dist/commands/reset.js +19 -0
  70. package/dist/commands/run.d.ts +16 -0
  71. package/dist/commands/run.js +56 -0
  72. package/dist/commands/runtime-env.d.ts +12 -0
  73. package/dist/commands/runtime-env.js +42 -0
  74. package/dist/commands/settings/get.d.ts +12 -0
  75. package/dist/commands/settings/get.js +67 -0
  76. package/dist/commands/settings/remove.d.ts +9 -0
  77. package/dist/commands/settings/remove.js +35 -0
  78. package/dist/commands/settings/set.d.ts +10 -0
  79. package/dist/commands/settings/set.js +39 -0
  80. package/dist/commands/setup-debug.d.ts +12 -0
  81. package/dist/commands/setup-debug.js +118 -0
  82. package/dist/commands/status.d.ts +6 -0
  83. package/dist/commands/status.js +22 -0
  84. package/dist/commands/team/pull.d.ts +8 -0
  85. package/dist/commands/team/pull.js +23 -0
  86. package/dist/commands/team/push.d.ts +11 -0
  87. package/dist/commands/team/push.js +41 -0
  88. package/dist/commands/team/status.d.ts +8 -0
  89. package/dist/commands/team/status.js +40 -0
  90. package/dist/commands/test/scaffold.d.ts +9 -0
  91. package/dist/commands/test/scaffold.js +61 -0
  92. package/dist/commands/test.d.ts +13 -0
  93. package/dist/commands/test.js +72 -0
  94. package/dist/commands/variables/add.d.ts +12 -0
  95. package/dist/commands/variables/add.js +21 -0
  96. package/dist/commands/variables/pull.d.ts +12 -0
  97. package/dist/commands/variables/pull.js +14 -0
  98. package/dist/commands/variables/push.d.ts +12 -0
  99. package/dist/commands/variables/push.js +14 -0
  100. package/dist/commands/variables/remove.d.ts +9 -0
  101. package/dist/commands/variables/remove.js +19 -0
  102. package/dist/commands/variables/reset.d.ts +9 -0
  103. package/dist/commands/variables/reset.js +13 -0
  104. package/dist/commands/variables/status.d.ts +12 -0
  105. package/dist/commands/variables/status.js +58 -0
  106. package/dist/config/global.d.ts +15 -0
  107. package/dist/config/global.js +77 -0
  108. package/dist/config/index.d.ts +14 -0
  109. package/dist/config/index.js +44 -0
  110. package/dist/config/remote.d.ts +15 -0
  111. package/dist/config/remote.js +60 -0
  112. package/dist/config/settings.d.ts +16 -0
  113. package/dist/config/settings.js +54 -0
  114. package/dist/config/workspace.d.ts +28 -0
  115. package/dist/config/workspace.js +158 -0
  116. package/dist/core/commands/api-client-command.d.ts +6 -0
  117. package/dist/core/commands/api-client-command.js +30 -0
  118. package/dist/core/commands/base-command.d.ts +11 -0
  119. package/dist/core/commands/base-command.js +61 -0
  120. package/dist/core/commands/dependency-command.d.ts +5 -0
  121. package/dist/core/commands/dependency-command.js +16 -0
  122. package/dist/core/commands/modules-command.d.ts +7 -0
  123. package/dist/core/commands/modules-command.js +18 -0
  124. package/dist/core/commands/processes-command.d.ts +8 -0
  125. package/dist/core/commands/processes-command.js +27 -0
  126. package/dist/core/commands/resource-command.d.ts +24 -0
  127. package/dist/core/commands/resource-command.js +92 -0
  128. package/dist/core/commands/variables-command.d.ts +5 -0
  129. package/dist/core/commands/variables-command.js +16 -0
  130. package/dist/core/dependencies/env-dependencies-command.d.ts +19 -0
  131. package/dist/core/dependencies/env-dependencies-command.js +70 -0
  132. package/dist/core/dependencies/env-process-dependencies-command.d.ts +15 -0
  133. package/dist/core/dependencies/env-process-dependencies-command.js +78 -0
  134. package/dist/core/dependencies/javascript-env-dependencies.d.ts +8 -0
  135. package/dist/core/dependencies/javascript-env-dependencies.js +77 -0
  136. package/dist/core/dependencies/python-env-dependencies.d.ts +9 -0
  137. package/dist/core/dependencies/python-env-dependencies.js +91 -0
  138. package/dist/core/errors.d.ts +55 -0
  139. package/dist/core/errors.js +144 -0
  140. package/dist/core/executors/execution-task.d.ts +29 -0
  141. package/dist/core/executors/execution-task.js +139 -0
  142. package/dist/core/executors/javascript-execution-task.d.ts +13 -0
  143. package/dist/core/executors/javascript-execution-task.js +618 -0
  144. package/dist/core/executors/python/context.d.ts +2 -0
  145. package/dist/core/executors/python/context.js +3 -0
  146. package/dist/core/executors/python/datastore.d.ts +2 -0
  147. package/dist/core/executors/python/datastore.js +4 -0
  148. package/dist/core/executors/python-execution-task.d.ts +11 -0
  149. package/dist/core/executors/python-execution-task.js +173 -0
  150. package/dist/core/executors/source-code-sanitizer.d.ts +4 -0
  151. package/dist/core/executors/source-code-sanitizer.js +45 -0
  152. package/dist/core/resource-merger.d.ts +14 -0
  153. package/dist/core/resource-merger.js +155 -0
  154. package/dist/core/resource-pusher.d.ts +12 -0
  155. package/dist/core/resource-pusher.js +133 -0
  156. package/dist/core/resource-repository.d.ts +8 -0
  157. package/dist/core/resource-repository.js +2 -0
  158. package/dist/core/types.d.ts +290 -0
  159. package/dist/core/types.js +238 -0
  160. package/dist/core/utils/dependencies.d.ts +5 -0
  161. package/dist/core/utils/dependencies.js +82 -0
  162. package/dist/core/utils/variables.d.ts +4 -0
  163. package/dist/core/utils/variables.js +10 -0
  164. package/dist/hooks/init/auth.d.ts +3 -0
  165. package/dist/hooks/init/auth.js +28 -0
  166. package/dist/hooks/init/update.d.ts +3 -0
  167. package/dist/hooks/init/update.js +42 -0
  168. package/dist/index.d.ts +1 -0
  169. package/dist/index.js +4 -0
  170. package/dist/repositories/process.d.ts +36 -0
  171. package/dist/repositories/process.js +339 -0
  172. package/dist/repositories/programming-language-manifest.d.ts +17 -0
  173. package/dist/repositories/programming-language-manifest.js +95 -0
  174. package/dist/repositories/resource.d.ts +29 -0
  175. package/dist/repositories/resource.js +79 -0
  176. package/dist/repositories/script-library.d.ts +28 -0
  177. package/dist/repositories/script-library.js +164 -0
  178. package/dist/repositories/team.d.ts +23 -0
  179. package/dist/repositories/team.js +74 -0
  180. package/dist/repositories/variable.d.ts +26 -0
  181. package/dist/repositories/variable.js +174 -0
  182. package/dist/runtime/executor.js +87 -0
  183. package/dist/runtime/executor.py +81 -0
  184. package/dist/runtime/libs/node/fcode/index.js +1 -0
  185. package/dist/runtime/libs/python/fcode/__init__.py +75 -0
  186. package/dist/runtime/libs/python/fcode/context.py +98 -0
  187. package/dist/runtime/libs/python/fcode/datastore.py +35 -0
  188. package/dist/runtime/libs/python/fcode/schedule.py +45 -0
  189. package/dist/runtime/libs/python/fcode/storage.py +271 -0
  190. package/dist/runtime/libs/python/fcode/variables.py +102 -0
  191. package/dist/services/auth-service.d.ts +12 -0
  192. package/dist/services/auth-service.js +114 -0
  193. package/dist/services/credential-prompter.d.ts +6 -0
  194. package/dist/services/credential-prompter.js +60 -0
  195. package/dist/services/fcode-graphql-client.d.ts +45 -0
  196. package/dist/services/fcode-graphql-client.js +721 -0
  197. package/dist/services/http-server.d.ts +3 -0
  198. package/dist/services/http-server.js +17 -0
  199. package/dist/services/oauth-browser-service.d.ts +9 -0
  200. package/dist/services/oauth-browser-service.js +164 -0
  201. package/dist/services/process-parameters-sampler.d.ts +2 -0
  202. package/dist/services/process-parameters-sampler.js +41 -0
  203. package/dist/services/process-runner.d.ts +22 -0
  204. package/dist/services/process-runner.js +97 -0
  205. package/dist/services/resource-checker.d.ts +8 -0
  206. package/dist/services/resource-checker.js +82 -0
  207. package/dist/services/test-runner.d.ts +17 -0
  208. package/dist/services/test-runner.js +228 -0
  209. package/dist/utils/canonical-hash.d.ts +9 -0
  210. package/dist/utils/canonical-hash.js +30 -0
  211. package/dist/utils/cli/index.d.ts +14 -0
  212. package/dist/utils/cli/index.js +50 -0
  213. package/dist/utils/cli/screen.d.ts +2 -0
  214. package/dist/utils/cli/screen.js +25 -0
  215. package/dist/utils/cli/styled/table.d.ts +23 -0
  216. package/dist/utils/cli/styled/table.js +272 -0
  217. package/dist/utils/colorize-resource.d.ts +1 -0
  218. package/dist/utils/colorize-resource.js +7 -0
  219. package/dist/utils/env-utils.d.ts +2 -0
  220. package/dist/utils/env-utils.js +16 -0
  221. package/dist/utils/file-utils.d.ts +11 -0
  222. package/dist/utils/file-utils.js +33 -0
  223. package/dist/utils/filenamify.d.ts +4 -0
  224. package/dist/utils/filenamify.js +73 -0
  225. package/dist/utils/gitignore.d.ts +17 -0
  226. package/dist/utils/gitignore.js +60 -0
  227. package/dist/utils/promise.d.ts +4 -0
  228. package/dist/utils/promise.js +38 -0
  229. package/dist/utils/url-utils.d.ts +2 -0
  230. package/dist/utils/url-utils.js +10 -0
  231. package/dist/utils/variable-warnings.d.ts +8 -0
  232. package/dist/utils/variable-warnings.js +38 -0
  233. package/oclif.manifest.json +1783 -0
  234. package/package.json +129 -0
@@ -0,0 +1,75 @@
1
+ import os
2
+ import json
3
+ import subprocess
4
+ import logging
5
+
6
+ YC_COMMAND = os.environ.get("YC_COMMAND", "fcode")
7
+
8
+ metadata = {
9
+ "execution": {"process": {"name": "", "id": ""}},
10
+ "parameters": {},
11
+ "variables": [],
12
+ }
13
+ runtime_metadata_path = os.environ.get("FCODE_RUNTIME_METADATA_PATH", "")
14
+ process_slug = os.environ.get("FCODE_PROCESS_SLUG", "")
15
+ if runtime_metadata_path:
16
+ os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
17
+ "[FCODE LIBRARY] Reading metadata from file: " + runtime_metadata_path
18
+ )
19
+ with open(runtime_metadata_path, "r") as f:
20
+ metadata = json.load(f)
21
+ else:
22
+ process_params = os.environ.get("FCODE_PROCESS_PARAMS", "")
23
+ process_version = os.environ.get("FCODE_PROCESS_VERSION", "")
24
+ os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
25
+ "[FCODE LIBRARY] Reading metadata from process-metadata command"
26
+ )
27
+ command = [YC_COMMAND, "process-metadata"]
28
+ if process_slug:
29
+ command.append(process_slug)
30
+ if process_slug and process_version:
31
+ command.append("-v")
32
+ command.append(process_version)
33
+ if process_params:
34
+ command.append("-p")
35
+ command.append(process_params)
36
+ os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
37
+ "[FCODE LIBRARY] Running command: " + " ".join(command)
38
+ )
39
+ result = subprocess.run(command, encoding="utf-8", stdout=subprocess.PIPE)
40
+ if result.returncode != 0:
41
+ os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
42
+ "[FCODE LIBRARY] Command finished with exit code: "
43
+ + str(result.returncode)
44
+ )
45
+ exit(1)
46
+ metadata = json.loads(result.stdout)
47
+
48
+ from fcode.context import FcodeGlobal, Context, ExecutionInfo, Environment
49
+ from fcode.datastore import Datastore
50
+ from fcode.storage import Storage
51
+ from fcode.schedule import Schedule
52
+ from fcode.variables import Variables
53
+
54
+ context = Context(metadata.get("parameters", {}))
55
+ execution = ExecutionInfo(metadata.get("execution", {}))
56
+ env_variables = Environment(metadata.get("variables", []))
57
+ import builtins
58
+
59
+ fcode = FcodeGlobal(
60
+ context,
61
+ execution,
62
+ env_variables,
63
+ Datastore(),
64
+ Storage(),
65
+ Schedule(),
66
+ Variables(env_variables),
67
+ )
68
+
69
+ logging.basicConfig(level=os.environ.get("FCODE_LOG_LEVEL", logging.INFO))
70
+ builtins.logging = logging
71
+ logger = logging.getLogger("fcode_logger")
72
+ builtins.fcode = fcode
73
+ builtins.logger = logger
74
+
75
+ __all__ = ["logging", "logger", "fcode"]
@@ -0,0 +1,98 @@
1
+ import sys
2
+ import os
3
+
4
+
5
+ class Context(dict):
6
+ def __init__(self, parameters):
7
+ self["parameters"] = parameters
8
+ self["request"] = "No request, you are using the CLI"
9
+
10
+ def __getattr__(self, attr):
11
+ return self.get(attr)
12
+
13
+
14
+ class AttrsInfo(dict):
15
+ def __init__(self, attrs):
16
+ if attrs:
17
+ for key, value in attrs.items():
18
+ self[key] = value
19
+
20
+ def __getattr__(self, attr):
21
+ return self.get(attr)
22
+
23
+
24
+ class ExecutionInfo(dict):
25
+ def __init__(self, parameters):
26
+ self["id"] = "00000000-0000-0000-0000-000000000000"
27
+ process = parameters.get("process", {})
28
+ self["process"] = AttrsInfo(
29
+ {
30
+ "id": process.get("id", "00000000-0000-0000-0000-000000000000"),
31
+ "name": process.get("name", "Process not yet pushed to cloud"),
32
+ }
33
+ )
34
+ self["timeout"] = 86400000
35
+
36
+ def __getattr__(self, attr):
37
+ return self.get(attr)
38
+
39
+
40
+ class Environment(dict):
41
+ def __init__(self, team_variables):
42
+ if team_variables:
43
+ for key, value in team_variables.items():
44
+ self[key] = value
45
+
46
+ def __getattr__(self, attr):
47
+ return self.get(attr)
48
+
49
+
50
+ class Processes(dict):
51
+ def run(self, process_identifier, options=None):
52
+ import subprocess
53
+ import json
54
+ import sys
55
+
56
+ if not process_identifier:
57
+ raise ValueError("Process identifier is required")
58
+
59
+ tag = f" -v {options['tag']}" if options and "tag" in options else ""
60
+ parameters = (
61
+ f" -p '{json.dumps(options.get('parameters', {}))}'" if options else ""
62
+ )
63
+
64
+ command = f"fcode run {process_identifier}{tag}{parameters}"
65
+ print(f"Executing command: {command}") # Debug line
66
+
67
+ try:
68
+ process = subprocess.Popen(
69
+ command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
70
+ )
71
+ stdout, stderr = process.communicate()
72
+
73
+ if process.returncode != 0:
74
+ print(stderr.decode(), file=sys.stderr)
75
+ print(stdout.decode())
76
+ print(stderr.decode(), file=sys.stderr)
77
+ except Exception as error:
78
+ print(error, file=sys.stderr)
79
+
80
+
81
+ class FcodeGlobal:
82
+ def __init__(self, context, execution, env, datastore, storage, schedule, variables):
83
+ self.context = context
84
+ self.execution = execution
85
+ self.team = AttrsInfo({"timezone": "UTC"})
86
+ self.env = env
87
+ self.datastore = datastore
88
+ self.storage = storage
89
+ self.schedule = schedule
90
+ self.variables = variables
91
+ self.processes = Processes()
92
+
93
+ def import_module(self, module_name, version=None):
94
+ module_path = module_name
95
+ if version:
96
+ module_path = os.path.join(module_path, "versions", version)
97
+ sys.path.append(os.path.join(os.getcwd(), "modules", module_path))
98
+ return __import__(module_name)
@@ -0,0 +1,35 @@
1
+ import json
2
+ import os
3
+
4
+
5
+ class Datastore:
6
+ def save(self, datastore):
7
+ path = os.path.join(os.getcwd(), "datastore.json")
8
+ with open(path, "w") as file:
9
+ json.dump(datastore, file, indent=2)
10
+
11
+ def getAll(self):
12
+ path = os.path.join(os.getcwd(), "datastore.json")
13
+ try:
14
+ with open(path, "r") as file:
15
+ return json.load(file)
16
+ except FileNotFoundError:
17
+ return {}
18
+
19
+ def get(self, key):
20
+ if isinstance(key, (str, int)):
21
+ return self.getAll().get(key, None)
22
+ raise TypeError("Invalid argument type")
23
+
24
+ def set(self, key, value):
25
+ if isinstance(key, (str, int)) and isinstance(value, (str, int)):
26
+ datastore = self.getAll()
27
+ datastore[key] = value
28
+ self.save(datastore)
29
+ else:
30
+ raise TypeError("Invalid argument type")
31
+
32
+ def delete(self, key):
33
+ datastore = self.getAll()
34
+ datastore.pop(key, None)
35
+ self.save(datastore)
@@ -0,0 +1,45 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("fcode_logger")
4
+
5
+ _CLOUD_ONLY = (
6
+ "[fcode] schedule.%s only runs in the cloud; it is a no-op during local "
7
+ "`fcode run`."
8
+ )
9
+
10
+
11
+ class Schedule:
12
+ """Local `fcode run` shim for `fcode.schedule`.
13
+
14
+ Scheduling is a cloud-only capability, so every method is a logged no-op
15
+ locally. Push the process and schedule it in the cloud to exercise it.
16
+ """
17
+
18
+ def create(self, process_id_or_slug, **kwargs):
19
+ logger.warning(_CLOUD_ONLY, "create")
20
+ return None
21
+
22
+ def list(self, process_id=None, keywords=None):
23
+ logger.warning(_CLOUD_ONLY, "list")
24
+ return []
25
+
26
+ def get(self, id):
27
+ logger.warning(_CLOUD_ONLY, "get")
28
+ return None
29
+
30
+ def update(self, id, **kwargs):
31
+ logger.warning(_CLOUD_ONLY, "update")
32
+ return None
33
+
34
+ def delete(self, id):
35
+ logger.warning(_CLOUD_ONLY, "delete")
36
+
37
+ def delete_for_process(self, process_id_or_slug):
38
+ logger.warning(_CLOUD_ONLY, "delete_for_process")
39
+ return 0
40
+
41
+ def pause(self, id):
42
+ logger.warning(_CLOUD_ONLY, "pause")
43
+
44
+ def resume(self, id):
45
+ logger.warning(_CLOUD_ONLY, "resume")
@@ -0,0 +1,271 @@
1
+ import os
2
+ import hashlib
3
+ import mimetypes
4
+ from datetime import datetime, timedelta, timezone
5
+ from pathlib import Path
6
+
7
+
8
+ class SignedUrl:
9
+ def __init__(self, url: str, path: str, expires_at: str):
10
+ self.url = url
11
+ self.path = path
12
+ self.expires_at = expires_at
13
+
14
+
15
+ class StorageObject:
16
+ def __init__(
17
+ self,
18
+ name: str,
19
+ size: int,
20
+ link: str,
21
+ md5_hash: str,
22
+ content_type: str,
23
+ created_at: str,
24
+ updated_at: str,
25
+ ):
26
+ self.name = name
27
+ self.size = size
28
+ self.md5_hash = md5_hash
29
+ self.content_type = content_type
30
+ self.created_at = created_at
31
+ self.updated_at = updated_at
32
+ self.link = link
33
+
34
+
35
+ class Storage:
36
+ def _create_storage_object(self, filename, file_path, md5_hash):
37
+ try:
38
+ stats = os.stat(file_path)
39
+ current_time = datetime.utcnow().isoformat() + "Z"
40
+
41
+ content_type, _ = mimetypes.guess_type(filename)
42
+ if not content_type:
43
+ content_type = "application/octet-stream"
44
+
45
+ return StorageObject(
46
+ name=filename,
47
+ size=stats.st_size,
48
+ link=file_path,
49
+ md5_hash=md5_hash.hexdigest(),
50
+ content_type=content_type,
51
+ created_at=current_time,
52
+ updated_at=current_time,
53
+ )
54
+ except Exception as error:
55
+ raise RuntimeError(
56
+ f"Storage upload failed: unable to process file - {error}"
57
+ )
58
+
59
+ def upload(self, filename, file_obj):
60
+ if not isinstance(filename, str):
61
+ raise TypeError("Storage upload failed: filename must be a string")
62
+
63
+ if not filename.strip():
64
+ raise ValueError("Storage upload failed: filename cannot be empty")
65
+
66
+ if file_obj is None:
67
+ raise ValueError("Storage upload failed: file parameter is required")
68
+
69
+ is_file_like = hasattr(file_obj, "read")
70
+ is_bytes = isinstance(file_obj, bytes)
71
+ is_string = isinstance(file_obj, str)
72
+
73
+ if not is_file_like and not is_bytes and not is_string:
74
+ raise TypeError(
75
+ "Storage upload failed: file must be file-like object, bytes, or string"
76
+ )
77
+
78
+ try:
79
+ storage_dir = os.path.join(os.getcwd(), "storage")
80
+ if not os.path.exists(storage_dir):
81
+ os.makedirs(storage_dir, exist_ok=True)
82
+
83
+ file_path = os.path.join(storage_dir, filename)
84
+ file_dir = os.path.dirname(file_path)
85
+
86
+ if not os.path.exists(file_dir):
87
+ os.makedirs(file_dir, exist_ok=True)
88
+
89
+ md5_hash = hashlib.md5()
90
+
91
+ with open(file_path, "wb") as f:
92
+ if is_file_like:
93
+ while chunk := file_obj.read(8192):
94
+ md5_hash.update(chunk)
95
+ f.write(chunk)
96
+ elif is_string:
97
+ content = file_obj.encode("utf-8")
98
+ md5_hash.update(content)
99
+ f.write(content)
100
+ else:
101
+ md5_hash.update(file_obj)
102
+ f.write(file_obj)
103
+
104
+ return self._create_storage_object(filename, file_path, md5_hash)
105
+
106
+ except (OSError, IOError) as error:
107
+ raise RuntimeError(f"Storage upload failed: {error}")
108
+ except Exception as error:
109
+ raise RuntimeError(f"Storage upload failed: {error}")
110
+
111
+ def list(self, prefix=""):
112
+ if not isinstance(prefix, str):
113
+ raise TypeError("Storage list failed: prefix must be a string")
114
+
115
+ storage_dir = os.path.join(os.getcwd(), "storage")
116
+
117
+ if not os.path.exists(storage_dir):
118
+ return []
119
+
120
+ files = []
121
+
122
+ try:
123
+ for root, dirs, file_names in os.walk(storage_dir):
124
+ for file_name in file_names:
125
+ full_path = os.path.join(root, file_name)
126
+ relative_path = os.path.relpath(full_path, storage_dir)
127
+
128
+ normalized_path = relative_path.replace(os.sep, "/")
129
+
130
+ if not prefix or normalized_path.startswith(prefix):
131
+ try:
132
+ stats = os.stat(full_path)
133
+
134
+ md5_hash = hashlib.md5()
135
+ with open(full_path, "rb") as f:
136
+ while chunk := f.read(8192):
137
+ md5_hash.update(chunk)
138
+
139
+ content_type, _ = mimetypes.guess_type(normalized_path)
140
+ if not content_type:
141
+ content_type = "application/octet-stream"
142
+
143
+ files.append(
144
+ StorageObject(
145
+ name=normalized_path,
146
+ size=stats.st_size,
147
+ link=full_path,
148
+ md5_hash=md5_hash.hexdigest(),
149
+ content_type=content_type,
150
+ created_at=datetime.fromtimestamp(
151
+ stats.st_birthtime
152
+ ).isoformat()
153
+ + "Z",
154
+ updated_at=datetime.fromtimestamp(
155
+ stats.st_mtime
156
+ ).isoformat()
157
+ + "Z",
158
+ )
159
+ )
160
+ except (OSError, IOError):
161
+ print(
162
+ f"Storage list warning: unable to process file {normalized_path}"
163
+ )
164
+ continue
165
+
166
+ except Exception as error:
167
+ raise RuntimeError(f"Storage list failed: {error}")
168
+
169
+ return files
170
+
171
+ def download(self, filename):
172
+ if not isinstance(filename, str):
173
+ raise TypeError("Storage download failed: filename must be a string")
174
+
175
+ if not filename.strip():
176
+ raise ValueError("Storage download failed: filename cannot be empty")
177
+
178
+ file_path = os.path.join(os.getcwd(), "storage", filename)
179
+
180
+ if not os.path.exists(file_path):
181
+ raise FileNotFoundError(
182
+ f"Storage download failed: file not found - {filename}"
183
+ )
184
+
185
+ if not os.path.isfile(file_path):
186
+ raise ValueError(f"Storage download failed: {filename} is not a file")
187
+
188
+ try:
189
+ with open(file_path, "rb") as f:
190
+ return f.read()
191
+ except (OSError, IOError) as error:
192
+ raise RuntimeError(
193
+ f"Storage download failed: unable to read file - {error}"
194
+ )
195
+
196
+ def delete(self, filename):
197
+ if not isinstance(filename, str):
198
+ raise TypeError("Storage delete failed: filename must be a string")
199
+
200
+ if not filename.strip():
201
+ raise ValueError("Storage delete failed: filename cannot be empty")
202
+
203
+ file_path = os.path.join(os.getcwd(), "storage", filename)
204
+
205
+ if not os.path.exists(file_path):
206
+ raise FileNotFoundError(
207
+ f"Storage delete failed: file not found - {filename}"
208
+ )
209
+
210
+ if not os.path.isfile(file_path):
211
+ raise ValueError(f"Storage delete failed: {filename} is not a file")
212
+
213
+ try:
214
+ os.remove(file_path)
215
+
216
+ dir_path = os.path.dirname(file_path)
217
+ storage_dir = os.path.join(os.getcwd(), "storage")
218
+
219
+ while dir_path != storage_dir and dir_path != os.path.dirname(dir_path):
220
+ try:
221
+ if not os.listdir(dir_path):
222
+ os.rmdir(dir_path)
223
+ dir_path = os.path.dirname(dir_path)
224
+ else:
225
+ break
226
+ except OSError as error:
227
+ print(
228
+ f"Storage delete warning: unable to clean up directory {dir_path}: {error}"
229
+ )
230
+ break
231
+
232
+ except (OSError, IOError) as error:
233
+ raise RuntimeError(f"Storage delete failed: {error}")
234
+
235
+ def create_signed_url(self, name, expires_in_seconds=None):
236
+ if not isinstance(name, str):
237
+ raise TypeError("Storage create_signed_url failed: name must be a string")
238
+
239
+ if not name.strip():
240
+ raise ValueError("Storage create_signed_url failed: name cannot be empty")
241
+
242
+ if expires_in_seconds is None:
243
+ expires_in_seconds = 3600
244
+ if not isinstance(expires_in_seconds, int) or not (
245
+ 1 <= expires_in_seconds <= 604800
246
+ ):
247
+ raise ValueError(
248
+ "Storage create_signed_url failed: expires_in_seconds must be an "
249
+ "integer between 1 and 604800"
250
+ )
251
+
252
+ file_path = os.path.join(os.getcwd(), "storage", name)
253
+
254
+ if not os.path.exists(file_path):
255
+ raise FileNotFoundError(
256
+ f"Storage create_signed_url failed: file not found - {name}"
257
+ )
258
+
259
+ # Local runtime has no object store to sign against, so return a file://
260
+ # URL to the file on disk. Mirrors the cloud { url, path, expires_at }
261
+ # contract so process code needs no local-vs-cloud guard.
262
+ print(
263
+ "Storage create_signed_url: no object store in local runtime; "
264
+ "returning a file:// URL to the local file (mock)."
265
+ )
266
+
267
+ expires_at = (
268
+ datetime.now(timezone.utc) + timedelta(seconds=expires_in_seconds)
269
+ ).isoformat()
270
+
271
+ return SignedUrl(url=Path(file_path).as_uri(), path=name, expires_at=expires_at)
@@ -0,0 +1,102 @@
1
+ import logging
2
+ import os
3
+
4
+ logger = logging.getLogger("fcode_logger")
5
+
6
+ LOCAL_VARIABLES_FILE = "variables.local.env"
7
+
8
+
9
+ class Variables:
10
+ """Local `fcode run` shim for `fcode.variables`.
11
+
12
+ Reads return the team + local variables loaded for this run, overlaid with the
13
+ current `variables.local.env`. Writes (set/delete) affect only
14
+ `variables.local.env` in the workspace root, never the shared `variables.env`.
15
+ The `sensitive` flag has no representation in the `.env` file and is ignored
16
+ locally.
17
+ """
18
+
19
+ def __init__(self, env):
20
+ self._base = dict(env or {})
21
+
22
+ def _local_path(self):
23
+ return os.path.join(os.getcwd(), LOCAL_VARIABLES_FILE)
24
+
25
+ def _read_local(self):
26
+ try:
27
+ with open(self._local_path(), "r") as file:
28
+ return file.read()
29
+ except FileNotFoundError:
30
+ return ""
31
+
32
+ def _parse(self, text):
33
+ result = {}
34
+ for line in text.splitlines():
35
+ stripped = line.strip()
36
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
37
+ continue
38
+ key, value = stripped.split("=", 1)
39
+ result[key.strip()] = value
40
+ return result
41
+
42
+ def _merged(self):
43
+ merged = dict(self._base)
44
+ merged.update(self._parse(self._read_local()))
45
+ return merged
46
+
47
+ def list(self):
48
+ return [{"key": key, "value": value} for key, value in self._merged().items()]
49
+
50
+ def get(self, key):
51
+ return self._merged().get(key)
52
+
53
+ def set(self, key, value, sensitive=True):
54
+ out = []
55
+ replaced = False
56
+ for line in self._read_local().splitlines():
57
+ stripped = line.strip()
58
+ if (
59
+ stripped
60
+ and not stripped.startswith("#")
61
+ and "=" in stripped
62
+ and stripped.split("=", 1)[0].strip() == key
63
+ ):
64
+ out.append(f"{key}={value}")
65
+ replaced = True
66
+ else:
67
+ out.append(line)
68
+ while out and out[-1].strip() == "":
69
+ out.pop()
70
+ if not replaced:
71
+ out.append(f"{key}={value}")
72
+ with open(self._local_path(), "w") as file:
73
+ file.write("\n".join(out) + "\n")
74
+ logger.info(
75
+ "[fcode] variables.set wrote %s to %s (local run).", key, LOCAL_VARIABLES_FILE
76
+ )
77
+
78
+ def delete(self, key):
79
+ text = self._read_local()
80
+ if not text:
81
+ return
82
+ out = []
83
+ for line in text.splitlines():
84
+ stripped = line.strip()
85
+ if (
86
+ stripped
87
+ and not stripped.startswith("#")
88
+ and "=" in stripped
89
+ and stripped.split("=", 1)[0].strip() == key
90
+ ):
91
+ continue
92
+ out.append(line)
93
+ while out and out[-1].strip() == "":
94
+ out.pop()
95
+ content = "\n".join(out) + "\n" if out else ""
96
+ with open(self._local_path(), "w") as file:
97
+ file.write(content)
98
+ logger.info(
99
+ "[fcode] variables.delete removed %s from %s (local run).",
100
+ key,
101
+ LOCAL_VARIABLES_FILE,
102
+ )
@@ -0,0 +1,12 @@
1
+ import type { AuthInfo, Credentials, Token, UserInfo } from '../core/types';
2
+ import GlobalConfig from '../config/global';
3
+ declare const AuthService: {
4
+ clientId: string;
5
+ getToken({ authUrl, email, password, clientId, clientSecret }: Credentials): Promise<Token>;
6
+ getUserInfo(authUrl: string, token: Token): Promise<UserInfo>;
7
+ globalConfig: GlobalConfig;
8
+ isAuthenticated(url: string): Promise<boolean>;
9
+ login({ url, authUrl, email, password, clientId, clientSecret, accessToken }: Credentials): Promise<AuthInfo>;
10
+ logout(url: string): Promise<void>;
11
+ };
12
+ export default AuthService;