@factorialco/fcode-cli 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/i18n/status.js +9 -0
  3. package/dist/commands/modules/pull.js +5 -4
  4. package/dist/commands/modules/push.js +1 -1
  5. package/dist/commands/modules/status.js +9 -0
  6. package/dist/commands/processes/pull.js +5 -4
  7. package/dist/commands/processes/push.js +1 -1
  8. package/dist/commands/processes/status.js +9 -0
  9. package/dist/commands/runtime-env.js +2 -2
  10. package/dist/core/commands/resource-command.d.ts +4 -1
  11. package/dist/core/commands/resource-command.js +9 -2
  12. package/dist/core/dependencies/javascript-env-dependencies.js +4 -4
  13. package/dist/core/dependencies/python-env-dependencies.js +5 -4
  14. package/dist/core/executors/execution-task.js +1 -1
  15. package/dist/core/executors/javascript-execution-task.js +21 -17
  16. package/dist/core/executors/python-execution-task.js +11 -10
  17. package/dist/core/resource-pusher.js +13 -7
  18. package/dist/core/types.d.ts +16 -3
  19. package/dist/core/types.js +31 -6
  20. package/dist/repositories/locale.js +5 -3
  21. package/dist/repositories/process.js +4 -2
  22. package/dist/repositories/script-library.js +3 -2
  23. package/dist/runtime/libs/python/fcode/datastore.py +3 -1
  24. package/dist/services/fcode-graphql-client.d.ts +26 -7
  25. package/dist/services/fcode-graphql-client.js +90 -41
  26. package/dist/services/skills-installer.js +2 -2
  27. package/dist/services/version-pull-reporter.d.ts +20 -0
  28. package/dist/services/version-pull-reporter.js +33 -0
  29. package/dist/utils/child-process.d.ts +26 -0
  30. package/dist/utils/child-process.js +60 -0
  31. package/oclif.manifest.json +1 -1
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @factorialco/fcode-cli
20
20
  $ fcode COMMAND
21
21
  running command...
22
22
  $ fcode (--version)
23
- @factorialco/fcode-cli/3.2.0 linux-x64 node-v22.22.3
23
+ @factorialco/fcode-cli/3.3.0 linux-x64 node-v22.22.3
24
24
  $ fcode --help [COMMAND]
25
25
  USAGE
26
26
  $ fcode COMMAND
@@ -7,6 +7,7 @@ const cli_1 = require("../../utils/cli");
7
7
  class I18nStatus extends i18n_command_1.default {
8
8
  async resourcesTableLog(resources) {
9
9
  const hasInherited = resources.some((resource) => resource.resolvingTeamSlug);
10
+ const hasDeleted = resources.some((resource) => resource.deletion);
10
11
  cli_1.cli.table(resources, {
11
12
  locale: {
12
13
  // The identifier, not the slug: an inherited file is tracked as `en.inherited`, and the
@@ -24,6 +25,14 @@ class I18nStatus extends i18n_command_1.default {
24
25
  header: 'inherited',
25
26
  },
26
27
  }),
28
+ ...(hasDeleted && {
29
+ deleted: {
30
+ get(resource) {
31
+ return (0, colorize_resource_1.colorizeByStatus)(resource.deletion ? `🪦 kept by ${resource.deletion.retainingVersionTags.join(', ')}` : '', resource.status);
32
+ },
33
+ header: 'deleted',
34
+ },
35
+ }),
27
36
  status: {
28
37
  get({ status }) {
29
38
  return (0, colorize_resource_1.colorizeByStatus)(status, status);
@@ -4,13 +4,14 @@ const color_1 = require("@oclif/color");
4
4
  const modules_command_1 = require("../../core/commands/modules-command");
5
5
  const types_1 = require("../../core/types");
6
6
  const cli_1 = require("../../utils/cli");
7
+ const version_pull_reporter_1 = require("../../services/version-pull-reporter");
7
8
  class ModulesPull extends modules_command_1.default {
8
9
  async pullVersioned() {
9
10
  const scriptLibraries = (await this._resourceRepository.findAll()).filter((scriptLibrary) => scriptLibrary.id);
10
11
  const scriptLibraryRepository = this._resourceRepository;
11
- const allPromises = [];
12
+ const pulls = [];
12
13
  for (const scriptLibrary of scriptLibraries) {
13
- allPromises.push(this._versionedResourceQuery(scriptLibrary.id).then(async (remoteVersions) => {
14
+ pulls.push((0, version_pull_reporter_1.trackVersionPull)(scriptLibrary.slug, this._versionedResourceQuery(scriptLibrary.id).then(async (remoteVersions) => {
14
15
  const remoteVersionTags = new Set(remoteVersions.map((v) => v.tag));
15
16
  // Remove local versions that were deleted in remote
16
17
  for (const localTag of scriptLibraryRepository.getLocalVersionTags(scriptLibrary)) {
@@ -26,10 +27,10 @@ class ModulesPull extends modules_command_1.default {
26
27
  await scriptLibraryRepository.saveVersion(scriptLibrary, versionedScriptLibrary);
27
28
  }
28
29
  }
29
- }));
30
+ })));
30
31
  await new Promise((resolve) => setTimeout(resolve, 10));
31
32
  }
32
- await Promise.allSettled(allPromises);
33
+ await (0, version_pull_reporter_1.reportFailedVersionPulls)(pulls, 'module');
33
34
  }
34
35
  async run() {
35
36
  await super.pullResources();
@@ -6,7 +6,7 @@ const types_1 = require("../../core/types");
6
6
  const cli_1 = require("../../utils/cli");
7
7
  class ModulesPush extends modules_command_1.default {
8
8
  async pushVersioned() {
9
- const scriptLibraries = (await this._resourceRepository.findAll()).filter((process) => process.id);
9
+ const scriptLibraries = (await this._resourceRepository.findAll()).filter((scriptLibrary) => scriptLibrary.id && !scriptLibrary.deletion);
10
10
  const scriptLibraryRepository = this._resourceRepository;
11
11
  const allPromises = [];
12
12
  for (const scriptLibrary of scriptLibraries) {
@@ -7,6 +7,7 @@ const cli_1 = require("../../utils/cli");
7
7
  class ModulesStatus extends modules_command_1.default {
8
8
  async resourcesTableLog(resources) {
9
9
  const hasInherited = resources.some((resource) => resource.resolvingTeamSlug);
10
+ const hasDeleted = resources.some((resource) => resource.deletion);
10
11
  cli_1.cli.table(resources, {
11
12
  name: {
12
13
  get({ slug, status }) {
@@ -22,6 +23,14 @@ class ModulesStatus extends modules_command_1.default {
22
23
  header: 'inherited',
23
24
  },
24
25
  }),
26
+ ...(hasDeleted && {
27
+ deleted: {
28
+ get(resource) {
29
+ return (0, colorize_resource_1.colorizeByStatus)(resource.deletion ? `🪦 kept by ${resource.deletion.retainingVersionTags.join(', ')}` : '', resource.status);
30
+ },
31
+ header: 'deleted',
32
+ },
33
+ }),
25
34
  status: {
26
35
  get({ status }) {
27
36
  return (0, colorize_resource_1.colorizeByStatus)(status, status);
@@ -5,13 +5,14 @@ const processes_command_1 = require("../../core/commands/processes-command");
5
5
  const types_1 = require("../../core/types");
6
6
  const cli_1 = require("../../utils/cli");
7
7
  const settings_1 = require("../../config/settings");
8
+ const version_pull_reporter_1 = require("../../services/version-pull-reporter");
8
9
  class ProcessesPull extends processes_command_1.default {
9
10
  async pullVersioned() {
10
11
  const processes = await this.getRemoteResources();
11
12
  const processRepository = this._resourceRepository;
12
- const allPromises = [];
13
+ const pulls = [];
13
14
  for (const process of processes) {
14
- allPromises.push(this._versionedResourceQuery(process.id).then(async (remoteVersions) => {
15
+ pulls.push((0, version_pull_reporter_1.trackVersionPull)(process.slug, this._versionedResourceQuery(process.id).then(async (remoteVersions) => {
15
16
  const remoteVersionTags = new Set(remoteVersions.map((v) => v.tag));
16
17
  // Remove local versions that were deleted in remote
17
18
  for (const localTag of processRepository.getLocalVersionTags(process)) {
@@ -27,10 +28,10 @@ class ProcessesPull extends processes_command_1.default {
27
28
  await processRepository.saveVersion(process, versionedProcess);
28
29
  }
29
30
  }
30
- }));
31
+ })));
31
32
  await new Promise((resolve) => setTimeout(resolve, 10));
32
33
  }
33
- await Promise.allSettled(allPromises);
34
+ await (0, version_pull_reporter_1.reportFailedVersionPulls)(pulls, 'process');
34
35
  }
35
36
  async run() {
36
37
  await super.pullResources();
@@ -7,7 +7,7 @@ const cli_1 = require("../../utils/cli");
7
7
  const settings_1 = require("../../config/settings");
8
8
  class ProcessesPush extends processes_command_1.default {
9
9
  async pushVersioned() {
10
- const processes = (await this._resourceRepository.findAll()).filter((process) => process.id);
10
+ const processes = (await this._resourceRepository.findAll()).filter((process) => process.id && !process.deletion);
11
11
  const processRepository = this._resourceRepository;
12
12
  const allPromises = [];
13
13
  for (const process of processes) {
@@ -7,6 +7,7 @@ const cli_1 = require("../../utils/cli");
7
7
  class ProcessesStatus extends processes_command_1.default {
8
8
  async resourcesTableLog(resources) {
9
9
  const hasInherited = resources.some((resource) => resource.resolvingTeamSlug);
10
+ const hasDeleted = resources.some((resource) => resource.deletion);
10
11
  cli_1.cli.table(resources, {
11
12
  slug: {
12
13
  get(resource) {
@@ -40,6 +41,14 @@ class ProcessesStatus extends processes_command_1.default {
40
41
  header: 'inherited',
41
42
  },
42
43
  }),
44
+ ...(hasDeleted && {
45
+ deleted: {
46
+ get(resource) {
47
+ return (0, colorize_resource_1.colorizeByStatus)(resource.deletion ? `🪦 kept by ${resource.deletion.retainingVersionTags.join(', ')}` : '', resource.status);
48
+ },
49
+ header: 'deleted',
50
+ },
51
+ }),
43
52
  status: {
44
53
  get({ status }) {
45
54
  return (0, colorize_resource_1.colorizeByStatus)(status, status);
@@ -21,10 +21,10 @@ class RuntimeEnv extends core_1.Command {
21
21
  console.log('DEPENDENCIES_PATH=' + dependenciesPath);
22
22
  console.log('LIBRARY_PATH=' + libraryPath);
23
23
  if (language === 'PYTHON') {
24
- console.log('PYTHONPATH=' + [dependenciesPath, libraryPath, process.cwd()].join(':'));
24
+ console.log('PYTHONPATH=' + [dependenciesPath, libraryPath, process.cwd()].join(path.delimiter));
25
25
  }
26
26
  else {
27
- console.log('NODE_PATH=' + [dependenciesPath, libraryPath, process.cwd()].join(':'));
27
+ console.log('NODE_PATH=' + [dependenciesPath, libraryPath, process.cwd()].join(path.delimiter));
28
28
  }
29
29
  }
30
30
  }
@@ -38,7 +38,10 @@ export default abstract class ResourceCommand<R extends Resource> extends ApiCli
38
38
  * whole set to place a change in context; `diffable` is only what a status says has changed.
39
39
  */
40
40
  protected diffSections(diffable: Array<ResourcePair<R>>, allPairs: Array<ResourcePair<R>>): Array<DiffSection>;
41
- /** Names the resource a patch belongs to, flagging an inherited one as the parent's to fix. */
41
+ /**
42
+ * Names the resource a patch belongs to, flagging an inherited one as the parent's to fix and a
43
+ * deleted one as kept only for its versions.
44
+ */
42
45
  protected diffLabel(pair: ResourcePair<R>): string;
43
46
  /**
44
47
  * Prints a unified diff of everything that differs between the workspace and the cloud, and
@@ -107,11 +107,18 @@ class ResourceCommand extends api_client_command_1.default {
107
107
  files: this._diffFilesFor(pair),
108
108
  }));
109
109
  }
110
- /** Names the resource a patch belongs to, flagging an inherited one as the parent's to fix. */
110
+ /**
111
+ * Names the resource a patch belongs to, flagging an inherited one as the parent's to fix and a
112
+ * deleted one as kept only for its versions.
113
+ */
111
114
  diffLabel(pair) {
112
115
  const label = `${pair.status} ${this._resourceRepository._repositoryName}/${pair.slug}`;
113
116
  const inheritedTeam = (0, resource_differ_1.inheritedFrom)(pair);
114
- return inheritedTeam ? `${label} šŸ”— inherited from '${inheritedTeam}' (read-only — run pull to update)` : label;
117
+ if (inheritedTeam) {
118
+ return `${label} šŸ”— inherited from '${inheritedTeam}' (read-only — run pull to update)`;
119
+ }
120
+ const deletion = (pair.remote ?? pair.local)?.deletion;
121
+ return deletion ? `${label} 🪦 deleted, kept by ${deletion.retainingVersionTags.join(', ')} (read-only — run pull to update)` : label;
115
122
  }
116
123
  /**
117
124
  * Prints a unified diff of everything that differs between the workspace and the cloud, and
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const node_child_process_1 = require("node:child_process");
4
3
  const fs = require("node:fs");
5
4
  const path = require("node:path");
6
5
  const env_dependencies_command_1 = require("./env-dependencies-command");
6
+ const child_process_1 = require("../../utils/child-process");
7
7
  class JavaScriptEnvDependencies extends env_dependencies_command_1.default {
8
8
  get programmingLanguage() {
9
9
  return 'JAVASCRIPT';
@@ -33,7 +33,7 @@ class JavaScriptEnvDependencies extends env_dependencies_command_1.default {
33
33
  // Backward compatibility
34
34
  const pnpmPath = await this.ensurePnpm();
35
35
  return new Promise((resolve, reject) => {
36
- const proc = (0, node_child_process_1.spawn)(pnpmPath, ['install', '--no-dev'], {
36
+ const proc = (0, child_process_1.spawnCommand)(pnpmPath, ['install', '--no-dev'], {
37
37
  cwd: installationPath,
38
38
  stdio: 'inherit',
39
39
  });
@@ -54,10 +54,10 @@ class JavaScriptEnvDependencies extends env_dependencies_command_1.default {
54
54
  }
55
55
  }
56
56
  async ensurePnpm() {
57
- const pnpmPath = path.join(this.envPath, 'node_modules', '.bin', 'pnpm');
57
+ const pnpmPath = (0, child_process_1.binShimPath)(path.join(this.envPath, 'node_modules'), 'pnpm');
58
58
  if (!fs.existsSync(pnpmPath)) {
59
59
  return new Promise((resolve, reject) => {
60
- const proc = (0, node_child_process_1.spawn)('npm', ['install', '--prefix', this.envPath, 'pnpm'], {
60
+ const proc = (0, child_process_1.spawnCommand)('npm', ['install', '--prefix', this.envPath, 'pnpm'], {
61
61
  cwd: this.envPath,
62
62
  stdio: 'inherit',
63
63
  });
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const node_child_process_1 = require("node:child_process");
4
4
  const fs = require("node:fs");
5
- const path = require("node:path");
6
5
  const env_dependencies_command_1 = require("./env-dependencies-command");
7
6
  const cli_1 = require("../../utils/cli");
7
+ const child_process_1 = require("../../utils/child-process");
8
8
  class PythonEnvDependencies extends env_dependencies_command_1.default {
9
9
  get programmingLanguage() {
10
10
  return 'PYTHON';
@@ -12,7 +12,8 @@ class PythonEnvDependencies extends env_dependencies_command_1.default {
12
12
  async createEnv() {
13
13
  try {
14
14
  await new Promise((resolve, reject) => {
15
- (0, node_child_process_1.exec)(`python3 -m venv ${this.envPath}`, (error) => {
15
+ const python = process.platform === 'win32' ? 'python' : 'python3';
16
+ (0, node_child_process_1.exec)(`${python} -m venv ${JSON.stringify(this.envPath)}`, (error) => {
16
17
  if (error) {
17
18
  reject(error);
18
19
  }
@@ -68,8 +69,8 @@ import json
68
69
  `;
69
70
  }
70
71
  async ensureUv() {
71
- const uvPath = path.join(this.envPath, 'bin', 'uv');
72
- const pipPath = path.join(this.envPath, 'bin', 'pip');
72
+ const uvPath = (0, child_process_1.venvExecutablePath)(this.envPath, 'uv');
73
+ const pipPath = (0, child_process_1.venvExecutablePath)(this.envPath, 'pip');
73
74
  if (!fs.existsSync(uvPath)) {
74
75
  return new Promise((resolve, reject) => {
75
76
  const proc = (0, node_child_process_1.spawn)(pipPath, ['install', '--force-reinstall', 'uv'], {
@@ -127,7 +127,7 @@ class ExecutionTask {
127
127
  const exportVars = {
128
128
  ...createFilesResult,
129
129
  FCODE_DEPENDENCIES_PATH: this._dependenciesPath,
130
- FCODE_EXECUTION_PATH: [createFilesResult.FCODE_LIBRARY_PATH, this._dependenciesPath].filter((p) => p).join(':'),
130
+ FCODE_EXECUTION_PATH: [createFilesResult.FCODE_LIBRARY_PATH, this._dependenciesPath].filter((p) => p).join(path.delimiter),
131
131
  };
132
132
  fs.writeFileSync(path.join(this._tempDir, 'METADATA'), Object.entries(exportVars)
133
133
  .map(([key, value]) => `${key}=${value}`)
@@ -6,6 +6,7 @@ const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const execution_task_1 = require("./execution-task");
8
8
  const source_code_sanitizer_1 = require("./source-code-sanitizer");
9
+ const child_process_1 = require("../../utils/child-process");
9
10
  /**
10
11
  * Task which handles native executions using child_process.
11
12
  */
@@ -18,11 +19,11 @@ class JavaScriptExecutionTask extends execution_task_1.default {
18
19
  const fcodeLibPath = path.join(__dirname, '..', '..', 'runtime', 'libs', 'node');
19
20
  const nodeModulesPath = path.join(this._dependenciesPath, 'node_modules');
20
21
  const forked = (0, node_child_process_1.fork)(path.resolve(this._execFilePath), {
21
- env: {
22
+ env: (0, child_process_1.childEnv)({
22
23
  FORCE_COLOR: 'true',
23
- NODE_PATH: [nodeModulesPath, nodePath, fcodeLibPath].filter((p) => p).join(':'),
24
+ NODE_PATH: [nodeModulesPath, nodePath, fcodeLibPath].filter((p) => p).join(path.delimiter),
24
25
  PATH: process.env.PATH,
25
- },
26
+ }),
26
27
  silent: false,
27
28
  });
28
29
  return new Promise((resolve, reject) => {
@@ -45,7 +46,7 @@ class JavaScriptExecutionTask extends execution_task_1.default {
45
46
  if (this._options.hookFilePath) {
46
47
  const fn = this._options.hookFunction ?? 'main';
47
48
  return `
48
- const _hooksModule = require('${this._options.hookFilePath}')
49
+ const _hooksModule = require(${JSON.stringify(this._options.hookFilePath)})
49
50
  const main = typeof _hooksModule['${fn}'] === 'function'
50
51
  ? _hooksModule['${fn}']
51
52
  : async () => undefined`;
@@ -56,7 +57,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
56
57
  }
57
58
  processPath.push('index.js');
58
59
  const fullPath = path.join(...processPath);
59
- return `const { main }= require('${fullPath}')`;
60
+ return `const { main }= require(${JSON.stringify(fullPath)})`;
60
61
  }
61
62
  async _createExecutionFiles(tempDir) {
62
63
  if (!this._options.silent) {
@@ -64,7 +65,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
64
65
  ? process.stdout.write(JSON.stringify({ timestamp: new Date().toISOString(), level: 'DEBUG', message: `Creating execution files for '${this._process.slug}' in ${tempDir}` }) + '\n')
65
66
  : console.debug(`Creating execution files for '${this._process.slug}' in ${tempDir}`);
66
67
  }
67
- const execFilePath = `${tempDir}/${this._process.slug}.js`;
68
+ const execFilePath = path.join(tempDir, `${this._process.slug}.js`);
68
69
  const metadata = this.getMetadata();
69
70
  const execution = metadata.execution;
70
71
  execution.id = '00000000-0000-0000-0000-000000000000';
@@ -117,7 +118,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
117
118
  const version = typeof tagOrOptions === 'string'
118
119
  ? tagOrOptions
119
120
  : (tagOrOptions && typeof tagOrOptions === 'object' ? tagOrOptions.version : undefined);
120
- let modulePath = require('path').join('${process.cwd()}', 'modules', moduleName);
121
+ let modulePath = require('path').join(${JSON.stringify(process.cwd())}, 'modules', moduleName);
121
122
  if(version) {
122
123
  modulePath = require('path').join(modulePath, 'versions', version)
123
124
  }
@@ -151,7 +152,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
151
152
  datastore: {
152
153
  async save(datastore) {
153
154
  return new Promise((resolve, reject) => {
154
- require('fs').writeFile(require('path').join('${process.cwd()}', 'datastore.json'), JSON.stringify(datastore, null, 2), (err) => {
155
+ require('fs').writeFile(require('path').join(${JSON.stringify(process.cwd())}, 'datastore.json'), JSON.stringify(datastore, null, 2), (err) => {
155
156
  if (err) {
156
157
  reject(err)
157
158
  }
@@ -161,7 +162,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
161
162
  },
162
163
  getAll() {
163
164
  try {
164
- return require(require('path').join('${process.cwd()}', 'datastore.json'));
165
+ return require(require('path').join(${JSON.stringify(process.cwd())}, 'datastore.json'));
165
166
  } catch (e) {
166
167
  return {}
167
168
  }
@@ -172,7 +173,10 @@ const main = typeof _hooksModule['${fn}'] === 'function'
172
173
  }
173
174
  throw new Error('Invalid argument type')
174
175
  },
175
- async set(key, value) {
176
+ async set(key, value, encrypted) {
177
+ if (encrypted !== undefined && typeof encrypted !== 'boolean') {
178
+ throw new Error('Invalid encrypted flag. Encrypted must be a boolean.')
179
+ }
176
180
  if ((typeof key === 'string' || typeof key === 'number') && (typeof value === 'string' || typeof value === 'number')) {
177
181
  const datastore = this.getAll()
178
182
  datastore[key] = value
@@ -235,7 +239,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
235
239
  }
236
240
 
237
241
  try {
238
- const storageDir = path.join('${process.cwd()}', 'storage')
242
+ const storageDir = path.join(${JSON.stringify(process.cwd())}, 'storage')
239
243
  if (!fs.existsSync(storageDir)) {
240
244
  fs.mkdirSync(storageDir)
241
245
  }
@@ -338,7 +342,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
338
342
  throw new Error('Storage list failed: prefix must be a string')
339
343
  }
340
344
 
341
- const storageDir = path.join('${process.cwd()}', 'storage')
345
+ const storageDir = path.join(${JSON.stringify(process.cwd())}, 'storage')
342
346
 
343
347
  if (!fs.existsSync(storageDir)) {
344
348
  return []
@@ -410,7 +414,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
410
414
  return reject(new Error('Storage download failed: filename cannot be empty'))
411
415
  }
412
416
 
413
- const filePath = path.join('${process.cwd()}', 'storage', filename)
417
+ const filePath = path.join(${JSON.stringify(process.cwd())}, 'storage', filename)
414
418
 
415
419
  if (!fs.existsSync(filePath)) {
416
420
  return reject(new Error(\`Storage download failed: file not found - \${filename}\`))
@@ -448,7 +452,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
448
452
  return reject(new Error('Storage delete failed: filename cannot be empty'))
449
453
  }
450
454
 
451
- const filePath = path.join('${process.cwd()}', 'storage', filename)
455
+ const filePath = path.join(${JSON.stringify(process.cwd())}, 'storage', filename)
452
456
 
453
457
  if (!fs.existsSync(filePath)) {
454
458
  return reject(new Error(\`Storage delete failed: file not found - \${filename}\`))
@@ -463,7 +467,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
463
467
  fs.unlinkSync(filePath)
464
468
 
465
469
  let dir = path.dirname(filePath)
466
- const storageDir = path.join('${process.cwd()}', 'storage')
470
+ const storageDir = path.join(${JSON.stringify(process.cwd())}, 'storage')
467
471
 
468
472
  while (dir !== storageDir && dir !== path.dirname(dir)) {
469
473
  try {
@@ -501,7 +505,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
501
505
  throw new Error('Storage createSignedUrl failed: expiresInSeconds must be an integer between 1 and 604800')
502
506
  }
503
507
 
504
- const filePath = path.join('${process.cwd()}', 'storage', filename)
508
+ const filePath = path.join(${JSON.stringify(process.cwd())}, 'storage', filename)
505
509
 
506
510
  if (!fs.existsSync(filePath)) {
507
511
  throw new Error(\`Storage createSignedUrl failed: file not found - \${filename}\`)
@@ -532,7 +536,7 @@ const main = typeof _hooksModule['${fn}'] === 'function'
532
536
  },
533
537
  variables: (() => {
534
538
  const fs = require('fs');
535
- const localEnvPath = require('path').join('${process.cwd()}', 'variables.local.env');
539
+ const localEnvPath = require('path').join(${JSON.stringify(process.cwd())}, 'variables.local.env');
536
540
  const baseVars = ${JSON.stringify(metadata.variables, null, 2)};
537
541
  const readLocal = () => { try { return fs.readFileSync(localEnvPath, 'utf8') } catch { return '' } };
538
542
  const parseEnv = (text) => {
@@ -7,15 +7,16 @@ const errors_1 = require("../errors");
7
7
  const execution_task_1 = require("./execution-task");
8
8
  const workspace_1 = require("../../config/workspace");
9
9
  const source_code_sanitizer_1 = require("./source-code-sanitizer");
10
+ const child_process_1 = require("../../utils/child-process");
10
11
  class PythonExecutionTask extends execution_task_1.default {
11
12
  async run() {
12
13
  if (!this._execFilePath) {
13
14
  this.prepare();
14
15
  }
15
16
  const venvPath = (0, workspace_1.baseDependenciesPath)(this._programmingLanguage);
16
- const forked = (0, node_child_process_1.spawn)(`${venvPath}/bin/python3`, ['-u', path.resolve(this._execFilePath)], {
17
+ const forked = (0, node_child_process_1.spawn)((0, child_process_1.venvPythonPath)(venvPath), ['-u', path.resolve(this._execFilePath)], {
17
18
  stdio: 'inherit',
18
- env: {},
19
+ env: (0, child_process_1.childEnv)({}),
19
20
  });
20
21
  return new Promise((resolve, reject) => {
21
22
  forked.on('exit', (code) => {
@@ -76,7 +77,7 @@ _builtins.print = _json_print
76
77
  return `import fcode as _imported_fcode_module
77
78
  ${jsonLogsSetup}
78
79
  import importlib.util as _importlib_util
79
- _spec = _importlib_util.spec_from_file_location("hooks", "${this._options.hookFilePath}")
80
+ _spec = _importlib_util.spec_from_file_location("hooks", ${JSON.stringify(this._options.hookFilePath)})
80
81
  _hooks_module = _importlib_util.module_from_spec(_spec)
81
82
  _spec.loader.exec_module(_hooks_module)
82
83
  _hook_fn = getattr(_hooks_module, '${fn}', None)
@@ -143,8 +144,8 @@ if (hasattr(process, 'main')):
143
144
  ? process.stdout.write(JSON.stringify({ timestamp: new Date().toISOString(), level: 'DEBUG', message: `Creating execution files for '${this._process.slug}' in ${tempDir}` }) + '\n')
144
145
  : console.debug(`Creating execution files for '${this._process.slug}' in ${tempDir}`);
145
146
  }
146
- const execFilePath = `${tempDir}/${this._process.slug}.py`;
147
- const libraryFilePath = `${tempDir}/initialize_params.py`;
147
+ const execFilePath = path.join(tempDir, `${this._process.slug}.py`);
148
+ const libraryFilePath = path.join(tempDir, 'initialize_params.py');
148
149
  fs.writeFileSync(libraryFilePath, JSON.stringify(this.getMetadata()));
149
150
  fs.writeFileSync(execFilePath, `
150
151
  import sys
@@ -152,13 +153,13 @@ import os
152
153
  import json
153
154
  import traceback
154
155
 
155
- sys.path.append('${path.join(this._dependenciesPath)}')
156
- sys.path.append('${path.join(__dirname, '..', '..', 'runtime', 'libs', 'python')}')
156
+ sys.path.append(${JSON.stringify(this._dependenciesPath)})
157
+ sys.path.append(${JSON.stringify(path.join(__dirname, '..', '..', 'runtime', 'libs', 'python'))})
157
158
 
158
159
  os.environ['FCODE_LOG_LEVEL'] = '${this._options.logLevel}'
159
- os.environ['FCODE_RUNTIME_METADATA_PATH'] = '${libraryFilePath}'
160
- os.environ['FCODE_RETURN_FILE'] = '${this.returnValueFilePath}'
161
- os.environ['FCODE_ERROR_FILE'] = '${this.errorValueFilePath}'
160
+ os.environ['FCODE_RUNTIME_METADATA_PATH'] = ${JSON.stringify(libraryFilePath)}
161
+ os.environ['FCODE_RETURN_FILE'] = ${JSON.stringify(this.returnValueFilePath)}
162
+ os.environ['FCODE_ERROR_FILE'] = ${JSON.stringify(this.errorValueFilePath)}
162
163
  ${this._options.jsonLogs ? `os.environ['FCODE_JSON_LOGS'] = 'true'` : ''}
163
164
  ${Object.entries(this.getVariablesObject()).map(([key, value]) => `if '${key}' not in os.environ: os.environ['${key}'] = ${JSON.stringify(value)}`).join('\n')}
164
165
 
@@ -15,14 +15,16 @@ class ResourcePusher {
15
15
  }
16
16
  async push(remoteResources, localResources, force) {
17
17
  cli_1.default.log(`\nšŸŒ©ļø Pushing ${this._repository._repositoryName}...`);
18
- for (const resource of localResources.filter((resource) => resource.resolvingTeamSlug)) {
18
+ // Neither an inherited resource nor a deleted one the cloud keeps for its versions is pushed:
19
+ // the first is the parent's, the second only exists through what was already published.
20
+ for (const resource of localResources.filter((resource) => (0, types_1.isReadOnly)(resource))) {
19
21
  if (await resource_checker_1.default.localResourceHasBeenModified(undefined, resource)) {
20
- cli_1.default.log(color_1.default.red(`🚨 '${resource.slug}' is inherited from team '${resource.resolvingTeamSlug}' and is read-only. ` +
22
+ cli_1.default.log(color_1.default.red(`🚨 '${resource.slug}' is ${(0, types_1.readOnlyReason)(resource)} and is read-only. ` +
21
23
  `Run 'fcode ${this._repository._repositoryName}:pull' to restore it before pushing.`));
22
24
  }
23
25
  }
24
- localResources = localResources.filter((resource) => !resource.resolvingTeamSlug);
25
- remoteResources = remoteResources.filter((resource) => !resource.resolvingTeamSlug);
26
+ localResources = localResources.filter((resource) => !(0, types_1.isReadOnly)(resource));
27
+ remoteResources = remoteResources.filter((resource) => !(0, types_1.isReadOnly)(resource));
26
28
  if (this._repository._repositoryName === 'variables') {
27
29
  const localVariables = localResources;
28
30
  const remoteVariables = remoteResources;
@@ -115,11 +117,12 @@ class ResourcePusher {
115
117
  }
116
118
  async removeResource(resource) {
117
119
  try {
120
+ let removed;
118
121
  if (this._repository._repositoryName === 'processes') {
119
- await this._fcodeGraphQLClient.removeProcess(resource.id);
122
+ removed = await this._fcodeGraphQLClient.removeProcess(resource.id);
120
123
  }
121
124
  if (this._repository._repositoryName === 'modules') {
122
- await this._fcodeGraphQLClient.removeScriptLibrary(resource.id);
125
+ removed = await this._fcodeGraphQLClient.removeScriptLibrary(resource.id);
123
126
  }
124
127
  if (this._repository._repositoryName === 'variables') {
125
128
  await this._fcodeGraphQLClient.removeVariable(resource.id);
@@ -127,7 +130,10 @@ class ResourcePusher {
127
130
  if (this._repository._repositoryName === 'i18n') {
128
131
  // By identifier, not slug: an inherited locale is tracked under a qualified slug, and only
129
132
  // this workspace's own locales — where the two coincide — ever reach a remote delete.
130
- await this._fcodeGraphQLClient.removeLocale(resource.locale);
133
+ removed = await this._fcodeGraphQLClient.removeLocale(resource.locale);
134
+ }
135
+ if (removed?.deletedAt) {
136
+ cli_1.default.log(color_1.default.yellow(`šŸ“Œ '${resource.slug}' is kept as deleted: workspace version(s) ${removed.retainingVersionTags.join(', ')} still contain it.`));
131
137
  }
132
138
  this._repository.removeMetadata(resource.slug);
133
139
  }
@@ -72,14 +72,27 @@ export interface WorkspaceConfig {
72
72
  export type ResourceRepositoryOptions = {
73
73
  name: string;
74
74
  };
75
+ /**
76
+ * Why a deleted resource is still in the cloud: a workspace version contains one of its published
77
+ * snapshots. The row is read-only there and goes for good once the last of those versions does.
78
+ */
79
+ export type Deletion = {
80
+ deletedAt: string;
81
+ retainingVersionTags: string[];
82
+ };
75
83
  export declare abstract class Resource {
76
84
  abstract readonly contentHash: string;
77
85
  id: string | undefined;
78
86
  slug: string;
79
87
  errorPushing: boolean | undefined;
80
88
  resolvingTeamSlug?: string;
89
+ deletion?: Deletion;
81
90
  sameAs(other: Resource): boolean;
82
91
  }
92
+ /** Whether the workspace may edit the resource: neither a parent's nor a deleted one is its to change. */
93
+ export declare const isReadOnly: (resource: Resource) => boolean;
94
+ /** The sentence `push` and `diff` use to say why a resource is not the workspace's to change. */
95
+ export declare const readOnlyReason: (resource: Resource) => string | undefined;
83
96
  export declare enum OnConflictResolution {
84
97
  KEEP_LOCAL = 0,
85
98
  KEEP_REMOTE = 1
@@ -185,7 +198,7 @@ export declare class Process extends Resource {
185
198
  dependencies: {
186
199
  scopedToProcess: boolean;
187
200
  };
188
- }, metadata: ProcessMetadataFields, resolvingTeamSlug?: string);
201
+ }, metadata: ProcessMetadataFields, resolvingTeamSlug?: string, deletion?: Deletion);
189
202
  get contentHash(): string;
190
203
  }
191
204
  export declare class VersionedProcess extends Resource {
@@ -207,7 +220,7 @@ export declare class ScriptLibrary extends Resource {
207
220
  name: string;
208
221
  programmingLanguage: ProgrammingLanguage;
209
222
  sourceCode: string;
210
- constructor(id: string, name: string, sourceCode: string, programmingLanguage: ProgrammingLanguage, resolvingTeamSlug?: string);
223
+ constructor(id: string, name: string, sourceCode: string, programmingLanguage: ProgrammingLanguage, resolvingTeamSlug?: string, deletion?: Deletion);
211
224
  get contentHash(): string;
212
225
  }
213
226
  export declare class Locale extends Resource {
@@ -217,7 +230,7 @@ export declare class Locale extends Resource {
217
230
  slug: string;
218
231
  locale: string;
219
232
  content: string;
220
- constructor(id: string | undefined, locale: string, content: string, resolvingTeamSlug?: string);
233
+ constructor(id: string | undefined, locale: string, content: string, resolvingTeamSlug?: string, deletion?: Deletion);
221
234
  /**
222
235
  * The name a locale is tracked and stored under. Unlike every other resource, the identifier is
223
236
  * not unique: a workspace sees its own `en` and each parent workspace's `en`, and all of them are