@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.
- package/README.md +24 -2
- package/dist/commands/app/push.d.ts +13 -0
- package/dist/commands/app/push.js +147 -0
- package/dist/commands/i18n/status.js +9 -0
- package/dist/commands/login.js +2 -1
- package/dist/commands/modules/pull.js +5 -4
- package/dist/commands/modules/push.js +1 -1
- package/dist/commands/modules/status.js +9 -0
- package/dist/commands/processes/pull.js +5 -4
- package/dist/commands/processes/push.js +1 -1
- package/dist/commands/processes/status.js +9 -0
- package/dist/commands/runtime-env.js +2 -2
- package/dist/commands/team/clone.js +1 -1
- package/dist/commands/team/pull.js +3 -2
- package/dist/commands/team/status.d.ts +2 -0
- package/dist/commands/team/status.js +44 -2
- package/dist/config/dev-team.d.ts +15 -0
- package/dist/config/dev-team.js +54 -1
- package/dist/config/settings.d.ts +1 -0
- package/dist/config/settings.js +6 -0
- package/dist/core/commands/resource-command.d.ts +4 -1
- package/dist/core/commands/resource-command.js +9 -2
- package/dist/core/dependencies/javascript-env-dependencies.js +4 -4
- package/dist/core/dependencies/python-env-dependencies.js +5 -4
- package/dist/core/executors/execution-task.js +1 -1
- package/dist/core/executors/javascript-execution-task.js +21 -17
- package/dist/core/executors/python-execution-task.js +11 -10
- package/dist/core/resource-pusher.js +13 -7
- package/dist/core/types.d.ts +16 -3
- package/dist/core/types.js +31 -6
- package/dist/hooks/init/auth.d.ts +19 -0
- package/dist/hooks/init/auth.js +40 -6
- package/dist/repositories/locale.js +5 -3
- package/dist/repositories/process.js +4 -2
- package/dist/repositories/script-library.js +3 -2
- package/dist/runtime/libs/python/fcode/datastore.py +3 -1
- package/dist/services/app-metadata-files.d.ts +98 -0
- package/dist/services/app-metadata-files.js +363 -0
- package/dist/services/dashboard-client.d.ts +59 -0
- package/dist/services/dashboard-client.js +116 -10
- package/dist/services/dev-team-service.d.ts +4 -1
- package/dist/services/dev-team-service.js +31 -17
- package/dist/services/fcode-graphql-client.d.ts +26 -7
- package/dist/services/fcode-graphql-client.js +90 -41
- package/dist/services/skills-installer.js +2 -2
- package/dist/services/version-pull-reporter.d.ts +20 -0
- package/dist/services/version-pull-reporter.js +33 -0
- package/dist/utils/child-process.d.ts +26 -0
- package/dist/utils/child-process.js +60 -0
- package/oclif.manifest.json +35 -2
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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,
|
|
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', '
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
72
|
-
const pipPath =
|
|
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(
|
|
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(
|
|
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 = `${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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)(
|
|
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",
|
|
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 = `${
|
|
147
|
-
const libraryFilePath =
|
|
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(
|
|
156
|
-
sys.path.append(
|
|
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'] =
|
|
160
|
-
os.environ['FCODE_RETURN_FILE'] =
|
|
161
|
-
os.environ['FCODE_ERROR_FILE'] =
|
|
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
|
-
|
|
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
|
|
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) => !
|
|
25
|
-
remoteResources = remoteResources.filter((resource) => !
|
|
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
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
package/dist/core/types.js
CHANGED
|
@@ -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;
|
package/dist/hooks/init/auth.js
CHANGED
|
@@ -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
|
|
16
|
-
const
|
|
17
|
-
|
|
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 (
|
|
21
|
-
await credential_prompter_1.default.prompt({ url, authUrl, email
|
|
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,
|
|
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
|
-
|
|
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 {
|