@factorialco/fcode-cli 3.1.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 (51) hide show
  1. package/README.md +24 -2
  2. package/dist/commands/app/push.d.ts +13 -0
  3. package/dist/commands/app/push.js +147 -0
  4. package/dist/commands/i18n/status.js +9 -0
  5. package/dist/commands/login.js +2 -1
  6. package/dist/commands/modules/pull.js +5 -4
  7. package/dist/commands/modules/push.js +1 -1
  8. package/dist/commands/modules/status.js +9 -0
  9. package/dist/commands/processes/pull.js +5 -4
  10. package/dist/commands/processes/push.js +1 -1
  11. package/dist/commands/processes/status.js +9 -0
  12. package/dist/commands/runtime-env.js +2 -2
  13. package/dist/commands/team/clone.js +1 -1
  14. package/dist/commands/team/pull.js +3 -2
  15. package/dist/commands/team/status.d.ts +2 -0
  16. package/dist/commands/team/status.js +44 -2
  17. package/dist/config/dev-team.d.ts +15 -0
  18. package/dist/config/dev-team.js +54 -1
  19. package/dist/config/settings.d.ts +1 -0
  20. package/dist/config/settings.js +6 -0
  21. package/dist/core/commands/resource-command.d.ts +4 -1
  22. package/dist/core/commands/resource-command.js +9 -2
  23. package/dist/core/dependencies/javascript-env-dependencies.js +4 -4
  24. package/dist/core/dependencies/python-env-dependencies.js +5 -4
  25. package/dist/core/executors/execution-task.js +1 -1
  26. package/dist/core/executors/javascript-execution-task.js +21 -17
  27. package/dist/core/executors/python-execution-task.js +11 -10
  28. package/dist/core/resource-pusher.js +13 -7
  29. package/dist/core/types.d.ts +16 -3
  30. package/dist/core/types.js +31 -6
  31. package/dist/hooks/init/auth.d.ts +19 -0
  32. package/dist/hooks/init/auth.js +40 -6
  33. package/dist/repositories/locale.js +5 -3
  34. package/dist/repositories/process.js +4 -2
  35. package/dist/repositories/script-library.js +3 -2
  36. package/dist/runtime/libs/python/fcode/datastore.py +3 -1
  37. package/dist/services/app-metadata-files.d.ts +98 -0
  38. package/dist/services/app-metadata-files.js +363 -0
  39. package/dist/services/dashboard-client.d.ts +59 -0
  40. package/dist/services/dashboard-client.js +116 -10
  41. package/dist/services/dev-team-service.d.ts +4 -1
  42. package/dist/services/dev-team-service.js +31 -17
  43. package/dist/services/fcode-graphql-client.d.ts +26 -7
  44. package/dist/services/fcode-graphql-client.js +90 -41
  45. package/dist/services/skills-installer.js +2 -2
  46. package/dist/services/version-pull-reporter.d.ts +20 -0
  47. package/dist/services/version-pull-reporter.js +33 -0
  48. package/dist/utils/child-process.d.ts +26 -0
  49. package/dist/utils/child-process.js +60 -0
  50. package/oclif.manifest.json +35 -2
  51. package/package.json +4 -1
@@ -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
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ServiceAccount = exports.Dependency = exports.DependenciesFlags = exports.TeamNameArg = exports.ShowInheritedFlag = exports.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.WorkspaceSettings = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedProgrammingLanguageManifest = exports.VersionedScriptLibrary = exports.Locale = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = exports.PROGRAMMING_LANGUAGES = exports.ResourceStatus = exports.OnConflictResolution = exports.Resource = void 0;
3
+ exports.ServiceAccount = exports.Dependency = exports.DependenciesFlags = exports.TeamNameArg = exports.ShowInheritedFlag = exports.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.WorkspaceSettings = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedProgrammingLanguageManifest = exports.VersionedScriptLibrary = exports.Locale = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = exports.PROGRAMMING_LANGUAGES = exports.ResourceStatus = exports.OnConflictResolution = exports.readOnlyReason = exports.isReadOnly = exports.Resource = void 0;
4
4
  const core_1 = require("@oclif/core");
5
5
  const crypto = require("node:crypto");
6
6
  const canonical_hash_1 = require("../utils/canonical-hash");
@@ -10,6 +10,27 @@ class Resource {
10
10
  }
11
11
  }
12
12
  exports.Resource = Resource;
13
+ /** Whether the workspace may edit the resource: neither a parent's nor a deleted one is its to change. */
14
+ const isReadOnly = (resource) => Boolean(resource.resolvingTeamSlug || resource.deletion);
15
+ exports.isReadOnly = isReadOnly;
16
+ /** The sentence `push` and `diff` use to say why a resource is not the workspace's to change. */
17
+ const readOnlyReason = (resource) => {
18
+ if (resource.resolvingTeamSlug) {
19
+ return `inherited from team '${resource.resolvingTeamSlug}'`;
20
+ }
21
+ if (resource.deletion) {
22
+ return `deleted in the cloud and kept by workspace version(s) ${resource.deletion.retainingVersionTags.join(', ')}`;
23
+ }
24
+ return undefined;
25
+ };
26
+ exports.readOnlyReason = readOnlyReason;
27
+ /**
28
+ * The deletion as it enters a content hash. A deleted resource must read as changed the moment the
29
+ * cloud deletes it — or brings it back — so a pull marks its files read-only (or writable again)
30
+ * even though their content did not move; a live resource contributes nothing, so no stored hash
31
+ * changes for it.
32
+ */
33
+ const deletionHashPart = (deletion) => deletion ? JSON.stringify({ deletedAt: deletion.deletedAt, retainingVersionTags: [...deletion.retainingVersionTags].sort() }) : '';
13
34
  var OnConflictResolution;
14
35
  (function (OnConflictResolution) {
15
36
  OnConflictResolution[OnConflictResolution["KEEP_LOCAL"] = 0] = "KEEP_LOCAL";
@@ -41,10 +62,11 @@ exports.programmingLanguageDisplayName = programmingLanguageDisplayName;
41
62
  // The header assumed when none is configured; its value carries a Bearer scheme.
42
63
  exports.DEFAULT_WEBHOOK_AUTH_HEADER = 'Authorization';
43
64
  class Process extends Resource {
44
- constructor(id, slug, script, readme, manifest, settings, metadata, resolvingTeamSlug) {
65
+ constructor(id, slug, script, readme, manifest, settings, metadata, resolvingTeamSlug, deletion) {
45
66
  super();
46
67
  this.id = id;
47
68
  this.slug = slug;
69
+ this.deletion = deletion;
48
70
  this.name = metadata.name;
49
71
  this.description = metadata.description;
50
72
  this.tags = metadata.tags;
@@ -72,6 +94,7 @@ class Process extends Resource {
72
94
  ...(this.uiTrigger && { uiTrigger: this.uiTrigger }),
73
95
  manifestDependencies: this.manifest.dependencies,
74
96
  dependenciesScopedToProcess: this.settings.dependencies.scopedToProcess,
97
+ ...(this.deletion && { deletion: deletionHashPart(this.deletion) }),
75
98
  });
76
99
  }
77
100
  }
@@ -94,7 +117,7 @@ class VersionedProcess extends Resource {
94
117
  }
95
118
  exports.VersionedProcess = VersionedProcess;
96
119
  class ScriptLibrary extends Resource {
97
- constructor(id, name, sourceCode, programmingLanguage, resolvingTeamSlug) {
120
+ constructor(id, name, sourceCode, programmingLanguage, resolvingTeamSlug, deletion) {
98
121
  super();
99
122
  this.id = id;
100
123
  this.slug = name;
@@ -102,20 +125,22 @@ class ScriptLibrary extends Resource {
102
125
  this.sourceCode = sourceCode;
103
126
  this.programmingLanguage = programmingLanguage;
104
127
  this.resolvingTeamSlug = resolvingTeamSlug;
128
+ this.deletion = deletion;
105
129
  }
106
130
  get contentHash() {
107
- return crypto.createHash('md5').update(this.sourceCode).digest('hex');
131
+ return crypto.createHash('md5').update(this.sourceCode).update(deletionHashPart(this.deletion)).digest('hex');
108
132
  }
109
133
  }
110
134
  exports.ScriptLibrary = ScriptLibrary;
111
135
  class Locale extends Resource {
112
- constructor(id, locale, content, resolvingTeamSlug) {
136
+ constructor(id, locale, content, resolvingTeamSlug, deletion) {
113
137
  super();
114
138
  this.id = id;
115
139
  this.slug = Locale.slugFor(locale, resolvingTeamSlug);
116
140
  this.locale = locale;
117
141
  this.content = content;
118
142
  this.resolvingTeamSlug = resolvingTeamSlug;
143
+ this.deletion = deletion;
119
144
  }
120
145
  /**
121
146
  * The name a locale is tracked and stored under. Unlike every other resource, the identifier is
@@ -133,7 +158,7 @@ class Locale extends Resource {
133
158
  // Hashed over the raw YAML: the file is the source of truth and round-trips comments and
134
159
  // ordering, so any edit — even one that parses to the same translations — is a change to push.
135
160
  get contentHash() {
136
- return crypto.createHash('md5').update(this.content).digest('hex');
161
+ return crypto.createHash('md5').update(this.content).update(deletionHashPart(this.deletion)).digest('hex');
137
162
  }
138
163
  }
139
164
  exports.Locale = Locale;
@@ -1,3 +1,22 @@
1
1
  import { Hook } from '@oclif/core';
2
+ import GlobalConfig from '../../config/global';
3
+ export type LoginMethod = 'accessToken' | 'credentials' | 'browser';
4
+ export interface LoginOptions {
5
+ email?: string;
6
+ password?: string;
7
+ clientId?: string;
8
+ clientSecret?: string;
9
+ accessToken?: string;
10
+ prompt?: boolean;
11
+ browserLoginDisabled?: boolean;
12
+ }
13
+ /**
14
+ * Picks how to log in from what the user passed. The browser OAuth flow is the default, but it is
15
+ * skipped when the user asked to be prompted (`--prompt`), when it is disabled through the
16
+ * `disableBrowserLogin` setting or the `FCODE_DISABLE_BROWSER_LOGIN` env var (headless machines,
17
+ * CI, remote shells), or when credentials that identify another method were given.
18
+ */
19
+ export declare function resolveLoginMethod(options: LoginOptions): LoginMethod;
20
+ export declare function isBrowserLoginDisabled(globalConfig?: GlobalConfig): Promise<boolean>;
2
21
  declare const auth: Hook<'init'>;
3
22
  export default auth;
@@ -1,27 +1,61 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveLoginMethod = resolveLoginMethod;
4
+ exports.isBrowserLoginDisabled = isBrowserLoginDisabled;
3
5
  const dotenv_1 = require("dotenv");
4
6
  const auth_service_1 = require("../../services/auth-service");
5
7
  const credential_prompter_1 = require("../../services/credential-prompter");
6
8
  const oauth_browser_service_1 = require("../../services/oauth-browser-service");
7
9
  const global_1 = require("../../config/global");
10
+ const settings_1 = require("../../config/settings");
8
11
  const url_utils_1 = require("../../utils/url-utils");
12
+ /**
13
+ * Picks how to log in from what the user passed. The browser OAuth flow is the default, but it is
14
+ * skipped when the user asked to be prompted (`--prompt`), when it is disabled through the
15
+ * `disableBrowserLogin` setting or the `FCODE_DISABLE_BROWSER_LOGIN` env var (headless machines,
16
+ * CI, remote shells), or when credentials that identify another method were given.
17
+ */
18
+ function resolveLoginMethod(options) {
19
+ if (options.accessToken) {
20
+ return 'accessToken';
21
+ }
22
+ if (options.prompt ||
23
+ options.browserLoginDisabled ||
24
+ options.email ||
25
+ options.password ||
26
+ (options.clientId && options.clientSecret)) {
27
+ return 'credentials';
28
+ }
29
+ return 'browser';
30
+ }
31
+ async function isBrowserLoginDisabled(globalConfig = new global_1.default()) {
32
+ return (process.env.FCODE_DISABLE_BROWSER_LOGIN === 'true' ||
33
+ (await globalConfig.getSetting(settings_1.SettingKeys.DISABLE_BROWSER_LOGIN)) === 'true');
34
+ }
9
35
  const auth = async function ({ argv }) {
10
36
  const url = argv[0] || global_1.FCODE_CLOUD_URL;
11
37
  const authUrl = argv[1] || (0, url_utils_1.buildAuthUrl)(url);
12
38
  const isAuthenticated = await auth_service_1.default.isAuthenticated(url);
13
39
  if (!isAuthenticated) {
14
40
  (0, dotenv_1.config)();
15
- const accessToken = argv[6];
16
- const prompt = argv[7];
17
- if (accessToken) {
41
+ const [, , email, password, clientId, clientSecret, accessToken, prompt] = argv;
42
+ const method = resolveLoginMethod({
43
+ email,
44
+ password,
45
+ clientId,
46
+ clientSecret,
47
+ accessToken,
48
+ prompt: Boolean(prompt),
49
+ browserLoginDisabled: await isBrowserLoginDisabled(),
50
+ });
51
+ if (method === 'accessToken') {
18
52
  await auth_service_1.default.login({ url, authUrl, accessToken });
19
53
  }
20
- else if (prompt || argv[2] || argv[3] || (argv[4] && argv[5])) {
21
- await credential_prompter_1.default.prompt({ url, authUrl, email: argv[2], password: argv[3], clientId: argv[4], clientSecret: argv[5] });
54
+ else if (method === 'credentials') {
55
+ await credential_prompter_1.default.prompt({ url, authUrl, email, password, clientId, clientSecret });
22
56
  }
23
57
  else {
24
- await (0, oauth_browser_service_1.loginViaBrowser)(url, authUrl, argv[4]);
58
+ await (0, oauth_browser_service_1.loginViaBrowser)(url, authUrl, clientId);
25
59
  }
26
60
  }
27
61
  };
@@ -32,7 +32,7 @@ class LocaleRepository extends resource_1.default {
32
32
  }
33
33
  try {
34
34
  const content = fs.readFileSync(this.getResourcePath(localeMetadata)).toString();
35
- return new types_1.Locale(localeMetadata.id, localeMetadata.locale ?? slug, content, localeMetadata.resolvingTeamSlug);
35
+ return new types_1.Locale(localeMetadata.id, localeMetadata.locale ?? slug, content, localeMetadata.resolvingTeamSlug, localeMetadata.deletion);
36
36
  }
37
37
  catch (error) {
38
38
  throw new errors_1.LocalLocaleNotFound(error, slug);
@@ -78,12 +78,14 @@ class LocaleRepository extends resource_1.default {
78
78
  locale: locale.locale,
79
79
  slug: locale.slug,
80
80
  ...(locale.resolvingTeamSlug && { resolvingTeamSlug: locale.resolvingTeamSlug }),
81
+ ...(locale.deletion && { deletion: locale.deletion }),
81
82
  };
82
83
  }
83
84
  async saveFiles(locale) {
84
85
  // Inherited locales are written read-only, like inherited modules: they belong to a parent
85
- // workspace, and editing one here would push an override rather than change the parent.
86
- (0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(locale), locale.content || '', { readOnly: Boolean(locale.resolvingTeamSlug) });
86
+ // workspace, and editing one here would push an override rather than change the parent. A
87
+ // deleted locale is read-only too: only its versions are still resolved.
88
+ (0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(locale), locale.content || '', { readOnly: (0, types_1.isReadOnly)(locale) });
87
89
  }
88
90
  async metadataFromFiles(slug) {
89
91
  return {