@factorialco/fcode-cli 2.4.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +332 -118
- package/dist/commands/clone.d.ts +0 -12
- package/dist/commands/clone.js +22 -135
- package/dist/commands/{settings → config}/get.d.ts +1 -1
- package/dist/commands/{settings → config}/get.js +7 -7
- package/dist/commands/{settings → config}/remove.d.ts +1 -1
- package/dist/commands/{settings → config}/remove.js +6 -6
- package/dist/commands/{settings → config}/set.d.ts +1 -1
- package/dist/commands/{settings → config}/set.js +8 -8
- package/dist/commands/dependencies/diff.d.ts +9 -0
- package/dist/commands/dependencies/diff.js +15 -0
- package/dist/commands/dependencies/pull.d.ts +9 -0
- package/dist/commands/dependencies/pull.js +44 -1
- package/dist/commands/diff.d.ts +12 -0
- package/dist/commands/diff.js +48 -0
- package/dist/commands/http.d.ts +1 -1
- package/dist/commands/http.js +7 -7
- package/dist/commands/i18n/diff.d.ts +9 -0
- package/dist/commands/i18n/diff.js +15 -0
- package/dist/commands/i18n/status.d.ts +3 -0
- package/dist/commands/i18n/status.js +1 -0
- package/dist/commands/modules/diff.d.ts +9 -0
- package/dist/commands/modules/diff.js +15 -0
- package/dist/commands/modules/status.d.ts +3 -0
- package/dist/commands/modules/status.js +1 -0
- package/dist/commands/processes/diff.d.ts +9 -0
- package/dist/commands/processes/diff.js +15 -0
- package/dist/commands/processes/status.d.ts +3 -0
- package/dist/commands/processes/status.js +1 -0
- package/dist/commands/pull.js +2 -2
- package/dist/commands/push.js +2 -2
- package/dist/commands/{team → settings}/aliases/delete.d.ts +1 -1
- package/dist/commands/{team → settings}/aliases/delete.js +6 -6
- package/dist/commands/{team → settings}/aliases/list.d.ts +1 -1
- package/dist/commands/{team → settings}/aliases/list.js +8 -8
- package/dist/commands/{team → settings}/aliases/set.d.ts +1 -1
- package/dist/commands/{team → settings}/aliases/set.js +8 -8
- package/dist/commands/settings/diff.d.ts +8 -0
- package/dist/commands/settings/diff.js +39 -0
- package/dist/commands/settings/pull.d.ts +11 -0
- package/dist/commands/settings/pull.js +41 -0
- package/dist/commands/{team → settings}/push.d.ts +4 -4
- package/dist/commands/{team → settings}/push.js +39 -39
- package/dist/commands/settings/status.d.ts +8 -0
- package/dist/commands/settings/status.js +46 -0
- package/dist/commands/{team → settings}/versions/create.d.ts +1 -1
- package/dist/commands/{team → settings}/versions/create.js +13 -10
- package/dist/commands/{team → settings}/versions/delete.d.ts +1 -1
- package/dist/commands/{team → settings}/versions/delete.js +10 -10
- package/dist/commands/{team → settings}/versions/list.d.ts +1 -1
- package/dist/commands/{team → settings}/versions/list.js +8 -8
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +10 -7
- package/dist/commands/team/clone.d.ts +14 -0
- package/dist/commands/team/clone.js +90 -0
- package/dist/commands/team/pull.d.ts +3 -7
- package/dist/commands/team/pull.js +24 -30
- package/dist/commands/team/status.d.ts +3 -4
- package/dist/commands/team/status.js +38 -33
- package/dist/commands/variables/diff.d.ts +9 -0
- package/dist/commands/variables/diff.js +15 -0
- package/dist/commands/variables/status.d.ts +3 -0
- package/dist/commands/variables/status.js +1 -0
- package/dist/config/dev-team.d.ts +64 -0
- package/dist/config/dev-team.js +76 -0
- package/dist/config/workspace.d.ts +2 -2
- package/dist/config/workspace.js +4 -2
- package/dist/core/commands/base-command.js +5 -0
- package/dist/core/commands/dependency-command.d.ts +2 -1
- package/dist/core/commands/dependency-command.js +3 -0
- package/dist/core/commands/i18n-command.js +2 -0
- package/dist/core/commands/modules-command.js +2 -0
- package/dist/core/commands/processes-command.js +2 -0
- package/dist/core/commands/resource-command.d.ts +26 -1
- package/dist/core/commands/resource-command.js +100 -3
- package/dist/core/commands/variables-command.d.ts +7 -0
- package/dist/core/commands/variables-command.js +13 -0
- package/dist/core/executors/javascript-execution-task.js +25 -3
- package/dist/core/types.d.ts +30 -7
- package/dist/core/types.js +38 -10
- package/dist/repositories/process.d.ts +8 -2
- package/dist/repositories/process.js +28 -18
- package/dist/repositories/programming-language-manifest.d.ts +15 -1
- package/dist/repositories/programming-language-manifest.js +45 -1
- package/dist/repositories/settings.d.ts +52 -0
- package/dist/repositories/settings.js +214 -0
- package/dist/runtime/libs/python/fcode/context.py +29 -2
- package/dist/services/dashboard-client.d.ts +43 -0
- package/dist/services/dashboard-client.js +108 -0
- package/dist/services/dev-team-service.d.ts +55 -0
- package/dist/services/dev-team-service.js +221 -0
- package/dist/services/fcode-graphql-client.d.ts +16 -2
- package/dist/services/fcode-graphql-client.js +56 -7
- package/dist/services/locale-resolver.d.ts +16 -5
- package/dist/services/locale-resolver.js +60 -12
- package/dist/services/resource-differ.d.ts +60 -0
- package/dist/services/resource-differ.js +135 -0
- package/dist/services/skills-installer.d.ts +16 -0
- package/dist/services/skills-installer.js +77 -0
- package/dist/services/team-versioning.d.ts +4 -4
- package/dist/services/team-versioning.js +4 -4
- package/dist/services/webhook-auth.js +1 -1
- package/dist/services/workspace-cloner.d.ts +31 -0
- package/dist/services/workspace-cloner.js +95 -0
- package/dist/utils/dev-slug.d.ts +12 -0
- package/dist/utils/dev-slug.js +37 -0
- package/dist/utils/diff.d.ts +19 -0
- package/dist/utils/diff.js +58 -0
- package/dist/utils/slugify.d.ts +17 -0
- package/dist/utils/slugify.js +41 -0
- package/oclif.manifest.json +428 -116
- package/package.json +12 -7
- package/dist/repositories/team.d.ts +0 -41
- package/dist/repositories/team.js +0 -170
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.manifestDiffFiles = exports.localeDiffFiles = exports.moduleDiffFiles = exports.processDiffFiles = exports.SETTINGS_DIFF_PATH = exports.INHERITED_VARIABLES_DIFF_PATH = exports.VARIABLES_DIFF_PATH = exports.SENSITIVE_PLACEHOLDER = void 0;
|
|
4
|
+
exports.pairResources = pairResources;
|
|
5
|
+
exports.inheritedFrom = inheritedFrom;
|
|
6
|
+
exports.processFiles = processFiles;
|
|
7
|
+
exports.moduleFiles = moduleFiles;
|
|
8
|
+
exports.localeFiles = localeFiles;
|
|
9
|
+
exports.manifestFiles = manifestFiles;
|
|
10
|
+
exports.variablesEnvDiffFiles = variablesEnvDiffFiles;
|
|
11
|
+
exports.settingsDiffFile = settingsDiffFile;
|
|
12
|
+
const resource_merger_1 = require("../core/resource-merger");
|
|
13
|
+
const dependencies_1 = require("../core/utils/dependencies");
|
|
14
|
+
const process_1 = require("../repositories/process");
|
|
15
|
+
const env_utils_1 = require("../utils/env-utils");
|
|
16
|
+
const filenamify_1 = require("../utils/filenamify");
|
|
17
|
+
/** Stands in for a variable's value wherever one side declares it sensitive, so no secret is ever printed. */
|
|
18
|
+
exports.SENSITIVE_PLACEHOLDER = '<sensitive value hidden>';
|
|
19
|
+
exports.VARIABLES_DIFF_PATH = 'variables.env';
|
|
20
|
+
/** What a workspace inherits is regenerated on pull into its own file, which the parent owns. */
|
|
21
|
+
exports.INHERITED_VARIABLES_DIFF_PATH = 'variables.inherited.env';
|
|
22
|
+
exports.SETTINGS_DIFF_PATH = 'settings.json';
|
|
23
|
+
async function pairResources(remoteResources, localResources, existsMetadata) {
|
|
24
|
+
const pairs = [];
|
|
25
|
+
const slugs = [...new Set([...localResources, ...remoteResources].map((resource) => resource.slug))];
|
|
26
|
+
for (const slug of slugs) {
|
|
27
|
+
const remote = remoteResources.find((resource) => resource.slug === slug);
|
|
28
|
+
const local = localResources.find((resource) => resource.slug === slug);
|
|
29
|
+
pairs.push({ slug, remote, local, status: await (0, resource_merger_1.mapResourceStatus)(remote, local, await existsMetadata(slug)) });
|
|
30
|
+
}
|
|
31
|
+
return pairs;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The workspace a resource is inherited from, when it is. An overridden variable resolves to
|
|
35
|
+
* undefined on its local side, which is what makes an override diffable while the parent's copy
|
|
36
|
+
* stays read-only.
|
|
37
|
+
*/
|
|
38
|
+
function inheritedFrom(pair) {
|
|
39
|
+
return (pair.local ?? pair.remote)?.resolvingTeamSlug;
|
|
40
|
+
}
|
|
41
|
+
function mergeSides(remoteFiles, localFiles) {
|
|
42
|
+
const paths = [...new Set([...Object.keys(remoteFiles), ...Object.keys(localFiles)])].sort();
|
|
43
|
+
return paths.map((path) => {
|
|
44
|
+
const file = { path };
|
|
45
|
+
if (path in remoteFiles) {
|
|
46
|
+
file.remote = remoteFiles[path];
|
|
47
|
+
}
|
|
48
|
+
if (path in localFiles) {
|
|
49
|
+
file.local = localFiles[path];
|
|
50
|
+
}
|
|
51
|
+
return file;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function sideFiles(pair, toFiles) {
|
|
55
|
+
return mergeSides(pair.remote ? toFiles(pair.remote) : {}, pair.local ? toFiles(pair.local) : {});
|
|
56
|
+
}
|
|
57
|
+
/** A process as the four files it is stored as, plus its own dependencies file when it scopes them. */
|
|
58
|
+
function processFiles(process) {
|
|
59
|
+
const directory = `processes/${(0, filenamify_1.default)(process.slug)}`;
|
|
60
|
+
const { programmingLanguage } = process.script;
|
|
61
|
+
const files = {
|
|
62
|
+
[`${directory}/${(0, process_1.PROCESS_SOURCE_CODE_FILE_NAME)(programmingLanguage)}`]: process.script.sourceCode || '',
|
|
63
|
+
[`${directory}/README.md`]: process.readme || '',
|
|
64
|
+
[`${directory}/metadata.json`]: JSON.stringify((0, process_1.buildProcessMetadata)(process), null, 2),
|
|
65
|
+
[`${directory}/parametersSchema.json`]: JSON.stringify(process.script.parametersSchema ?? {}, null, 2),
|
|
66
|
+
};
|
|
67
|
+
if (process.settings?.dependencies?.scopedToProcess) {
|
|
68
|
+
files[`${directory}/${(0, process_1.PROCESS_DEPENDENCIES_FILE_NAME)(programmingLanguage)}`] = (0, dependencies_1.dependenciesToFile)(programmingLanguage, process.manifest?.dependencies);
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
function moduleFiles(module) {
|
|
73
|
+
const name = (0, filenamify_1.default)(module.name);
|
|
74
|
+
const extension = module.programmingLanguage === 'PYTHON' ? 'py' : 'js';
|
|
75
|
+
return { [`modules/${name}/${name}.${extension}`]: module.sourceCode || '' };
|
|
76
|
+
}
|
|
77
|
+
function localeFiles(locale) {
|
|
78
|
+
return { [`i18n/${locale.slug}.yaml`]: locale.content || '' };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Only the workspace's own dependencies, matching what `contentHash` covers and what the manifest
|
|
82
|
+
* file holds: the inherited ones live in their own file and belong to a parent.
|
|
83
|
+
*/
|
|
84
|
+
function manifestFiles(manifest) {
|
|
85
|
+
const fileName = manifest.programmingLanguage === 'JAVASCRIPT' ? 'package.json' : 'requirements.txt';
|
|
86
|
+
return { [`dependencies/${fileName}`]: (0, dependencies_1.dependenciesToFile)(manifest.programmingLanguage, manifest.dependencies) };
|
|
87
|
+
}
|
|
88
|
+
const processDiffFiles = (pair) => sideFiles(pair, processFiles);
|
|
89
|
+
exports.processDiffFiles = processDiffFiles;
|
|
90
|
+
const moduleDiffFiles = (pair) => sideFiles(pair, moduleFiles);
|
|
91
|
+
exports.moduleDiffFiles = moduleDiffFiles;
|
|
92
|
+
const localeDiffFiles = (pair) => sideFiles(pair, localeFiles);
|
|
93
|
+
exports.localeDiffFiles = localeDiffFiles;
|
|
94
|
+
const manifestDiffFiles = (pair) => sideFiles(pair, manifestFiles);
|
|
95
|
+
exports.manifestDiffFiles = manifestDiffFiles;
|
|
96
|
+
function renderVariablesFile(pairs, path, include) {
|
|
97
|
+
const sensitiveKeys = new Set(pairs.filter((pair) => pair.remote?.isSensitive || pair.local?.isSensitive).map((pair) => pair.slug));
|
|
98
|
+
const renderSide = (side) => pairs
|
|
99
|
+
.map(side)
|
|
100
|
+
.filter((variable) => variable !== undefined && include(variable))
|
|
101
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
102
|
+
.map((variable) => `${variable.key}=${sensitiveKeys.has(variable.key) ? exports.SENSITIVE_PLACEHOLDER : (0, env_utils_1.escapeEnvValue)(variable.value || '')}`)
|
|
103
|
+
.join('\n');
|
|
104
|
+
return { path, remote: renderSide((pair) => pair.remote), local: renderSide((pair) => pair.local) };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Every variable as the .env files they share, rather than one pseudo-file each: that is how they
|
|
108
|
+
* are stored, and it keeps a rename or a reordering readable as one patch.
|
|
109
|
+
*
|
|
110
|
+
* The workspace's own keys and the inherited ones are separate files, because they are separate
|
|
111
|
+
* files on disk — a drifted inherited variable is a stale `variables.inherited.env` that a pull
|
|
112
|
+
* regenerates, not something to push. The inherited file only appears when something is inherited.
|
|
113
|
+
*
|
|
114
|
+
* A key either side marks sensitive is rendered as a placeholder on both, so a diff never prints a
|
|
115
|
+
* secret and a value-only change to a sensitive variable shows up as nothing (its status does).
|
|
116
|
+
*/
|
|
117
|
+
function variablesEnvDiffFiles(pairs) {
|
|
118
|
+
const isInherited = (variable) => Boolean(variable.resolvingTeamSlug);
|
|
119
|
+
const files = [renderVariablesFile(pairs, exports.VARIABLES_DIFF_PATH, (variable) => !isInherited(variable))];
|
|
120
|
+
if (pairs.some((pair) => (pair.remote && isInherited(pair.remote)) || (pair.local && isInherited(pair.local)))) {
|
|
121
|
+
files.push(renderVariablesFile(pairs, exports.INHERITED_VARIABLES_DIFF_PATH, isInherited));
|
|
122
|
+
}
|
|
123
|
+
return files;
|
|
124
|
+
}
|
|
125
|
+
/** The singleton workspace settings file. Both sides arrive already serialized by `SettingsRepository.toFile`. */
|
|
126
|
+
function settingsDiffFile(remote, local) {
|
|
127
|
+
const file = { path: exports.SETTINGS_DIFF_PATH };
|
|
128
|
+
if (remote !== undefined) {
|
|
129
|
+
file.remote = JSON.stringify(remote, null, 2);
|
|
130
|
+
}
|
|
131
|
+
if (local !== undefined) {
|
|
132
|
+
file.local = JSON.stringify(local, null, 2);
|
|
133
|
+
}
|
|
134
|
+
return file;
|
|
135
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installs the Factorial Code agent skills into `targetDir`. The skills CLI writes the skills
|
|
3
|
+
* themselves under `.agents/skills/` and symlinks them from `.claude/skills/`; it only installs
|
|
4
|
+
* to agents it detects, so `.claude/` is created first to make sure Claude Code is targeted.
|
|
5
|
+
*/
|
|
6
|
+
export declare function installSkillsAt(targetDir: string): Promise<void>;
|
|
7
|
+
export declare function skillsInstalledAt(rootDir: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Points `targetDir`'s `.claude/skills` at the ones installed in `rootDir`, so a team clone
|
|
10
|
+
* installs the skills once and every App still resolves them.
|
|
11
|
+
*
|
|
12
|
+
* The link is relative, so moving the whole tree keeps it working, and the per-skill symlinks
|
|
13
|
+
* inside it resolve against the real directory — not against the link — so they stay valid.
|
|
14
|
+
* Returns false when there is nothing to link to (the install failed, and it is not fatal).
|
|
15
|
+
*/
|
|
16
|
+
export declare function linkSkillsInto(targetDir: string, rootDir: string): boolean;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installSkillsAt = installSkillsAt;
|
|
4
|
+
exports.skillsInstalledAt = skillsInstalledAt;
|
|
5
|
+
exports.linkSkillsInto = linkSkillsInto;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const fs = require("fs-extra");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const cli_1 = require("../utils/cli");
|
|
10
|
+
// Skill sources installed on clone. `release-sdk` in factorial-api-sdks is a
|
|
11
|
+
// maintainer-only skill, so we pick `factorial-api-sdks` explicitly there.
|
|
12
|
+
const SKILLS_SOURCES = [
|
|
13
|
+
{ repo: 'factorialco/factorial-code-skills', skills: '*' },
|
|
14
|
+
{ repo: 'factorialco/factorial-api-sdks', skills: 'factorial-api-sdks' },
|
|
15
|
+
];
|
|
16
|
+
/** Where the skills CLI symlinks the installed skills for Claude Code to find them. */
|
|
17
|
+
const CLAUDE_SKILLS_PATH = path.join('.claude', 'skills');
|
|
18
|
+
function skillsInstallHint(repo, skills) {
|
|
19
|
+
cli_1.default.log('💡 Install the Factorial Code agent skills later with:');
|
|
20
|
+
cli_1.default.log(` npx skills add ${repo} -s '${skills}' -y`);
|
|
21
|
+
}
|
|
22
|
+
async function installSkills(repo, skills, cwd) {
|
|
23
|
+
// `npx --yes skills add <repo> -s <skills> -y` — non-interactive install with
|
|
24
|
+
// the skills CLI defaults: project scope (no -g), symlinked (no --copy).
|
|
25
|
+
const skillsArgs = ['--yes', 'skills', 'add', repo, '-s', skills, '-y'];
|
|
26
|
+
await new Promise((resolve) => {
|
|
27
|
+
const child = (0, node_child_process_1.spawn)('npx', skillsArgs, { cwd, stdio: 'inherit' });
|
|
28
|
+
child.on('error', () => {
|
|
29
|
+
// npx not available (or failed to launch) — don't fail the clone, just tell the user how to do it.
|
|
30
|
+
cli_1.default.log("\n⚠️ Couldn't run npx automatically (is Node.js / npx installed?).");
|
|
31
|
+
skillsInstallHint(repo, skills);
|
|
32
|
+
resolve();
|
|
33
|
+
});
|
|
34
|
+
child.on('close', (code) => {
|
|
35
|
+
if (code !== 0) {
|
|
36
|
+
cli_1.default.log("\n⚠️ Skill installation didn't complete.");
|
|
37
|
+
skillsInstallHint(repo, skills);
|
|
38
|
+
}
|
|
39
|
+
resolve();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Installs the Factorial Code agent skills into `targetDir`. The skills CLI writes the skills
|
|
45
|
+
* themselves under `.agents/skills/` and symlinks them from `.claude/skills/`; it only installs
|
|
46
|
+
* to agents it detects, so `.claude/` is created first to make sure Claude Code is targeted.
|
|
47
|
+
*/
|
|
48
|
+
async function installSkillsAt(targetDir) {
|
|
49
|
+
cli_1.default.log('');
|
|
50
|
+
fs.ensureDirSync(path.join(targetDir, '.claude'));
|
|
51
|
+
for (const source of SKILLS_SOURCES) {
|
|
52
|
+
await installSkills(source.repo, source.skills, targetDir);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function skillsInstalledAt(rootDir) {
|
|
56
|
+
return fs.pathExistsSync(path.join(rootDir, CLAUDE_SKILLS_PATH));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Points `targetDir`'s `.claude/skills` at the ones installed in `rootDir`, so a team clone
|
|
60
|
+
* installs the skills once and every App still resolves them.
|
|
61
|
+
*
|
|
62
|
+
* The link is relative, so moving the whole tree keeps it working, and the per-skill symlinks
|
|
63
|
+
* inside it resolve against the real directory — not against the link — so they stay valid.
|
|
64
|
+
* Returns false when there is nothing to link to (the install failed, and it is not fatal).
|
|
65
|
+
*/
|
|
66
|
+
function linkSkillsInto(targetDir, rootDir) {
|
|
67
|
+
if (!skillsInstalledAt(rootDir)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const link = path.join(targetDir, CLAUDE_SKILLS_PATH);
|
|
71
|
+
if (fs.pathExistsSync(link) || fs.lstatSync(link, { throwIfNoEntry: false })) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
fs.ensureDirSync(path.dirname(link));
|
|
75
|
+
fs.symlinkSync(path.relative(path.dirname(link), path.join(rootDir, CLAUDE_SKILLS_PATH)), link, 'dir');
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TeamAliasFileEntry, TeamVersion, TeamVersionAlias, TeamVersionFileEntry, WorkspaceSettings } from '../core/types';
|
|
2
2
|
/**
|
|
3
|
-
* What a
|
|
3
|
+
* What a settings:push must do to make the cloud's versions and aliases match settings.json.
|
|
4
4
|
* Versions are only ever created or deleted (tags are immutable snapshots); aliases are
|
|
5
5
|
* name->tag pointers, so a changed tag is an upsert of the same name.
|
|
6
6
|
*/
|
|
@@ -10,7 +10,7 @@ export interface TeamVersioningSyncPlan {
|
|
|
10
10
|
setAliases: TeamAliasFileEntry[];
|
|
11
11
|
deleteAliases: TeamVersionAlias[];
|
|
12
12
|
}
|
|
13
|
-
export declare function planTeamVersioningSync(
|
|
13
|
+
export declare function planTeamVersioningSync(settings: WorkspaceSettings, cloudVersions: TeamVersion[], cloudAliases: TeamVersionAlias[]): TeamVersioningSyncPlan;
|
|
14
14
|
export declare function teamVersioningSyncPlanIsEmpty(plan: TeamVersioningSyncPlan): boolean;
|
|
15
|
-
/** Logs the per-entity outcome of publishing a
|
|
15
|
+
/** Logs the per-entity outcome of publishing a workspace version and returns how many failed. */
|
|
16
16
|
export declare function logTeamVersionManifest(teamVersion: TeamVersion): number;
|
|
@@ -5,9 +5,9 @@ exports.teamVersioningSyncPlanIsEmpty = teamVersioningSyncPlanIsEmpty;
|
|
|
5
5
|
exports.logTeamVersionManifest = logTeamVersionManifest;
|
|
6
6
|
const color_1 = require("@oclif/color");
|
|
7
7
|
const cli_1 = require("../utils/cli");
|
|
8
|
-
function planTeamVersioningSync(
|
|
9
|
-
const localVersions =
|
|
10
|
-
const localAliases =
|
|
8
|
+
function planTeamVersioningSync(settings, cloudVersions, cloudAliases) {
|
|
9
|
+
const localVersions = settings.versions ?? [];
|
|
10
|
+
const localAliases = settings.aliases ?? [];
|
|
11
11
|
const cloudVersionTags = new Set(cloudVersions.map(({ tag }) => tag));
|
|
12
12
|
const localVersionTags = new Set(localVersions.map(({ tag }) => tag));
|
|
13
13
|
const cloudAliasesByName = new Map(cloudAliases.map((alias) => [alias.name, alias]));
|
|
@@ -25,7 +25,7 @@ function teamVersioningSyncPlanIsEmpty(plan) {
|
|
|
25
25
|
plan.setAliases.length === 0 &&
|
|
26
26
|
plan.deleteAliases.length === 0);
|
|
27
27
|
}
|
|
28
|
-
/** Logs the per-entity outcome of publishing a
|
|
28
|
+
/** Logs the per-entity outcome of publishing a workspace version and returns how many failed. */
|
|
29
29
|
function logTeamVersionManifest(teamVersion) {
|
|
30
30
|
const created = teamVersion.manifest.filter(({ outcome }) => outcome === 'CREATED');
|
|
31
31
|
const skipped = teamVersion.manifest.filter(({ outcome }) => outcome === 'SKIPPED_EXISTS');
|
|
@@ -37,7 +37,7 @@ function resolveEffectiveAuth(webhook, teamWebhookAuth) {
|
|
|
37
37
|
}
|
|
38
38
|
if (authMode === 'TEAM') {
|
|
39
39
|
if (!teamWebhookAuth) {
|
|
40
|
-
throw new Error('this webhook inherits the workspace webhook auth, which is not set in
|
|
40
|
+
throw new Error('this webhook inherits the workspace webhook auth, which is not set in settings.json');
|
|
41
41
|
}
|
|
42
42
|
return teamWebhookAuth;
|
|
43
43
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TeamOrigin } from '../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Creating and syncing a workspace directory, independent of the command that asks for it, so
|
|
4
|
+
* `fcode clone` (one workspace) and `fcode team:clone` (one per App) share the same behaviour.
|
|
5
|
+
*
|
|
6
|
+
* Everything below a resource command resolves the workspace from `process.cwd()` — the
|
|
7
|
+
* repositories, `WorkspaceConfig`, and the GraphQL client bound to the workspace slug through the
|
|
8
|
+
* `Fcode-Team` header. `runInWorkspace` is therefore the seam: it moves into the directory,
|
|
9
|
+
* runs the command, and always moves back. Commands run sequentially and each `Command.run()`
|
|
10
|
+
* builds a fresh instance, so every workspace gets its own config and its own client.
|
|
11
|
+
*/
|
|
12
|
+
/** Runs `body` with the process rooted at `workspaceDir`, restoring the previous cwd afterwards. */
|
|
13
|
+
export declare function runInWorkspace<T>(workspaceDir: string, body: () => Promise<T>): Promise<T>;
|
|
14
|
+
export declare class WorkspaceNotEmpty extends Error {
|
|
15
|
+
readonly workspaceDir: string;
|
|
16
|
+
constructor(workspaceDir: string);
|
|
17
|
+
}
|
|
18
|
+
export declare class WorkspaceAlreadyCloned extends Error {
|
|
19
|
+
readonly workspaceDir: string;
|
|
20
|
+
constructor(workspaceDir: string);
|
|
21
|
+
}
|
|
22
|
+
/** True when the directory already holds a Factorial Code workspace (`.fcode/workspace.json`). */
|
|
23
|
+
export declare function isClonedWorkspace(workspaceDir: string): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Lays out an empty workspace at `workspaceDir`: the resource directories, the `.fcode` metadata
|
|
26
|
+
* pointing at `origin`, and the files a pull expects to already exist.
|
|
27
|
+
*
|
|
28
|
+
* Throws `WorkspaceAlreadyCloned` / `WorkspaceNotEmpty` rather than exiting, so a caller cloning
|
|
29
|
+
* many workspaces can report the failure and carry on with the rest.
|
|
30
|
+
*/
|
|
31
|
+
export declare function scaffoldWorkspace(workspaceDir: string, origin: TeamOrigin): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkspaceAlreadyCloned = exports.WorkspaceNotEmpty = void 0;
|
|
4
|
+
exports.runInWorkspace = runInWorkspace;
|
|
5
|
+
exports.isClonedWorkspace = isClonedWorkspace;
|
|
6
|
+
exports.scaffoldWorkspace = scaffoldWorkspace;
|
|
7
|
+
const fs = require("fs-extra");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const global_1 = require("../config/global");
|
|
10
|
+
const workspace_1 = require("../config/workspace");
|
|
11
|
+
const settings_1 = require("../config/settings");
|
|
12
|
+
/**
|
|
13
|
+
* Creating and syncing a workspace directory, independent of the command that asks for it, so
|
|
14
|
+
* `fcode clone` (one workspace) and `fcode team:clone` (one per App) share the same behaviour.
|
|
15
|
+
*
|
|
16
|
+
* Everything below a resource command resolves the workspace from `process.cwd()` — the
|
|
17
|
+
* repositories, `WorkspaceConfig`, and the GraphQL client bound to the workspace slug through the
|
|
18
|
+
* `Fcode-Team` header. `runInWorkspace` is therefore the seam: it moves into the directory,
|
|
19
|
+
* runs the command, and always moves back. Commands run sequentially and each `Command.run()`
|
|
20
|
+
* builds a fresh instance, so every workspace gets its own config and its own client.
|
|
21
|
+
*/
|
|
22
|
+
/** Runs `body` with the process rooted at `workspaceDir`, restoring the previous cwd afterwards. */
|
|
23
|
+
async function runInWorkspace(workspaceDir, body) {
|
|
24
|
+
const previousCwd = process.cwd();
|
|
25
|
+
process.chdir(workspaceDir);
|
|
26
|
+
try {
|
|
27
|
+
return await body();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
process.chdir(previousCwd);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
class WorkspaceNotEmpty extends Error {
|
|
34
|
+
constructor(workspaceDir) {
|
|
35
|
+
super(`Destination path '${workspaceDir}' already exists and is not empty.`);
|
|
36
|
+
this.workspaceDir = workspaceDir;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.WorkspaceNotEmpty = WorkspaceNotEmpty;
|
|
40
|
+
class WorkspaceAlreadyCloned extends Error {
|
|
41
|
+
constructor(workspaceDir) {
|
|
42
|
+
super(`Workspace '${workspaceDir}' already cloned.`);
|
|
43
|
+
this.workspaceDir = workspaceDir;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.WorkspaceAlreadyCloned = WorkspaceAlreadyCloned;
|
|
47
|
+
/** True when the directory already holds a Factorial Code workspace (`.fcode/workspace.json`). */
|
|
48
|
+
async function isClonedWorkspace(workspaceDir) {
|
|
49
|
+
try {
|
|
50
|
+
await new workspace_1.default(workspaceDir).getActiveTeam();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function ignoreWorkspaceNotEmpty() {
|
|
58
|
+
const envValue = process.env.IGNORE_WORKSPACE_NOT_EMPTY;
|
|
59
|
+
if (envValue === 'true' || envValue === 'false') {
|
|
60
|
+
return envValue === 'true';
|
|
61
|
+
}
|
|
62
|
+
return (await new global_1.default().getSetting(settings_1.SettingKeys.IGNORE_WORKSPACE_NOT_EMPTY)) === 'true';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Lays out an empty workspace at `workspaceDir`: the resource directories, the `.fcode` metadata
|
|
66
|
+
* pointing at `origin`, and the files a pull expects to already exist.
|
|
67
|
+
*
|
|
68
|
+
* Throws `WorkspaceAlreadyCloned` / `WorkspaceNotEmpty` rather than exiting, so a caller cloning
|
|
69
|
+
* many workspaces can report the failure and carry on with the rest.
|
|
70
|
+
*/
|
|
71
|
+
async function scaffoldWorkspace(workspaceDir, origin) {
|
|
72
|
+
if (fs.pathExistsSync(workspaceDir)) {
|
|
73
|
+
// Listed up front, and `.fcode` probed only when it is already there: constructing a
|
|
74
|
+
// WorkspaceConfig creates the directory, which would make an empty target read as occupied.
|
|
75
|
+
const entries = fs.readdirSync(workspaceDir);
|
|
76
|
+
if (entries.includes('.fcode') && (await isClonedWorkspace(workspaceDir))) {
|
|
77
|
+
throw new WorkspaceAlreadyCloned(workspaceDir);
|
|
78
|
+
}
|
|
79
|
+
if (entries.length > 0 && !(await ignoreWorkspaceNotEmpty())) {
|
|
80
|
+
throw new WorkspaceNotEmpty(workspaceDir);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const globalConfig = new global_1.default();
|
|
84
|
+
const variablesFileName = await globalConfig.getSetting(settings_1.SettingKeys.VARIABLES_FILE_NAME);
|
|
85
|
+
const localVariablesFileName = await globalConfig.getSetting(settings_1.SettingKeys.LOCAL_VARIABLES_FILE_NAME);
|
|
86
|
+
fs.mkdirpSync(workspaceDir);
|
|
87
|
+
for (const directory of ['processes', 'modules', 'dependencies', 'i18n', 'storage']) {
|
|
88
|
+
fs.mkdirpSync(path.join(workspaceDir, directory));
|
|
89
|
+
}
|
|
90
|
+
fs.createFileSync(path.join(workspaceDir, variablesFileName));
|
|
91
|
+
fs.outputFileSync(path.join(workspaceDir, localVariablesFileName), '');
|
|
92
|
+
fs.outputFileSync(path.join(workspaceDir, 'datastore.json'), '{}');
|
|
93
|
+
fs.outputFileSync(path.join(workspaceDir, '.gitignore'), [localVariablesFileName, 'datastore.json', '.env', 'storage/*'].join('\n'));
|
|
94
|
+
await new workspace_1.default(workspaceDir).init(origin);
|
|
95
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace slugs embed an App's UUID base36-encoded: the platform derives a Postgres database
|
|
3
|
+
* name from the slug and a raw UUID overflows the 63-char identifier limit, so 128 bits become
|
|
4
|
+
* 25 lowercase alphanumeric characters.
|
|
5
|
+
*
|
|
6
|
+
* Ported from the dashboard's `lib/slug.ts` because no REST endpoint exposes the slug — the
|
|
7
|
+
* dashboard builds it from the App id the same way. The round-trip test pins them together.
|
|
8
|
+
*/
|
|
9
|
+
export declare function encodeUuid(uuid: string): string;
|
|
10
|
+
export declare function decodeUuid(token: string): string;
|
|
11
|
+
/** The development workspace of an App: what `fcode clone` takes and `team:clone` clones per app. */
|
|
12
|
+
export declare const devSlug: (appId: string) => string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Workspace slugs embed an App's UUID base36-encoded: the platform derives a Postgres database
|
|
4
|
+
* name from the slug and a raw UUID overflows the 63-char identifier limit, so 128 bits become
|
|
5
|
+
* 25 lowercase alphanumeric characters.
|
|
6
|
+
*
|
|
7
|
+
* Ported from the dashboard's `lib/slug.ts` because no REST endpoint exposes the slug — the
|
|
8
|
+
* dashboard builds it from the App id the same way. The round-trip test pins them together.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.devSlug = void 0;
|
|
12
|
+
exports.encodeUuid = encodeUuid;
|
|
13
|
+
exports.decodeUuid = decodeUuid;
|
|
14
|
+
const TOKEN_LENGTH = 25; // ceil(128 / log2(36))
|
|
15
|
+
const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
|
16
|
+
function encodeUuid(uuid) {
|
|
17
|
+
const hex = uuid.replaceAll('-', '').toLowerCase();
|
|
18
|
+
if (!/^[\da-f]{32}$/.test(hex)) {
|
|
19
|
+
throw new Error(`Not a UUID: ${uuid}`);
|
|
20
|
+
}
|
|
21
|
+
return BigInt(`0x${hex}`).toString(36).padStart(TOKEN_LENGTH, '0');
|
|
22
|
+
}
|
|
23
|
+
function decodeUuid(token) {
|
|
24
|
+
let value = 0n;
|
|
25
|
+
for (const char of token.toLowerCase()) {
|
|
26
|
+
const digit = ALPHABET.indexOf(char);
|
|
27
|
+
if (digit === -1) {
|
|
28
|
+
throw new Error(`Invalid slug token: ${token}`);
|
|
29
|
+
}
|
|
30
|
+
value = value * 36n + BigInt(digit);
|
|
31
|
+
}
|
|
32
|
+
const hex = value.toString(16).padStart(32, '0');
|
|
33
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
34
|
+
}
|
|
35
|
+
/** The development workspace of an App: what `fcode clone` takes and `team:clone` clones per app. */
|
|
36
|
+
const devSlug = (appId) => `dev-${encodeUuid(appId)}`;
|
|
37
|
+
exports.devSlug = devSlug;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const DEV_NULL = "/dev/null";
|
|
2
|
+
/**
|
|
3
|
+
* One comparable artifact of a resource, named after the file it is stored in. A side left
|
|
4
|
+
* undefined does not exist there (a process only in the cloud, a module only on disk); an empty
|
|
5
|
+
* string is a file that exists and is empty, which is a different thing and diffs differently.
|
|
6
|
+
*/
|
|
7
|
+
export type PseudoFile = {
|
|
8
|
+
path: string;
|
|
9
|
+
remote?: string;
|
|
10
|
+
local?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const hasDifference: (file: PseudoFile) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A unified diff of one pseudo-file, remote as the old side and local as the new one, so the
|
|
15
|
+
* patch reads as what a push would apply to the cloud. Empty when the sides are equal.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderUnifiedDiff(file: PseudoFile): string;
|
|
18
|
+
/** Colors a rendered patch the way git does. Chalk no-ops when the output is not a TTY. */
|
|
19
|
+
export declare function colorizeDiff(patch: string): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasDifference = exports.DEV_NULL = void 0;
|
|
4
|
+
exports.renderUnifiedDiff = renderUnifiedDiff;
|
|
5
|
+
exports.colorizeDiff = colorizeDiff;
|
|
6
|
+
const diff_1 = require("diff");
|
|
7
|
+
const chalk = require("chalk");
|
|
8
|
+
exports.DEV_NULL = '/dev/null';
|
|
9
|
+
const hasDifference = (file) => file.remote !== file.local;
|
|
10
|
+
exports.hasDifference = hasDifference;
|
|
11
|
+
/**
|
|
12
|
+
* A hunk range the way git writes one: the count is left out when it is 1, and an empty range
|
|
13
|
+
* points at the line it would be inserted after, so a whole-file addition reads `-0,0`.
|
|
14
|
+
*/
|
|
15
|
+
function formatRange(start, lines) {
|
|
16
|
+
if (lines === 1) {
|
|
17
|
+
return `${start}`;
|
|
18
|
+
}
|
|
19
|
+
return `${lines === 0 ? start - 1 : start},${lines}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A unified diff of one pseudo-file, remote as the old side and local as the new one, so the
|
|
23
|
+
* patch reads as what a push would apply to the cloud. Empty when the sides are equal.
|
|
24
|
+
*/
|
|
25
|
+
function renderUnifiedDiff(file) {
|
|
26
|
+
if (!(0, exports.hasDifference)(file)) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
const oldName = file.remote === undefined ? exports.DEV_NULL : `remote/${file.path}`;
|
|
30
|
+
const newName = file.local === undefined ? exports.DEV_NULL : `local/${file.path}`;
|
|
31
|
+
const { hunks } = (0, diff_1.structuredPatch)(oldName, newName, file.remote ?? '', file.local ?? '', undefined, undefined, { context: 3 });
|
|
32
|
+
const lines = [`--- ${oldName}`, `+++ ${newName}`];
|
|
33
|
+
for (const hunk of hunks) {
|
|
34
|
+
lines.push(`@@ -${formatRange(hunk.oldStart, hunk.oldLines)} +${formatRange(hunk.newStart, hunk.newLines)} @@`, ...hunk.lines);
|
|
35
|
+
}
|
|
36
|
+
return lines.join('\n');
|
|
37
|
+
}
|
|
38
|
+
/** Colors a rendered patch the way git does. Chalk no-ops when the output is not a TTY. */
|
|
39
|
+
function colorizeDiff(patch) {
|
|
40
|
+
return patch
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => {
|
|
43
|
+
if (line.startsWith('---') || line.startsWith('+++')) {
|
|
44
|
+
return chalk.bold(line);
|
|
45
|
+
}
|
|
46
|
+
if (line.startsWith('@@')) {
|
|
47
|
+
return chalk.cyan(line);
|
|
48
|
+
}
|
|
49
|
+
if (line.startsWith('+')) {
|
|
50
|
+
return chalk.green(line);
|
|
51
|
+
}
|
|
52
|
+
if (line.startsWith('-')) {
|
|
53
|
+
return chalk.red(line);
|
|
54
|
+
}
|
|
55
|
+
return line;
|
|
56
|
+
})
|
|
57
|
+
.join('\n');
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folder name for a development team or an App, derived from its display name. Names are free
|
|
3
|
+
* text — accents, punctuation, emoji — while the folder has to be portable and typeable, so
|
|
4
|
+
* everything outside `[a-z0-9-]` collapses into a single dash.
|
|
5
|
+
*/
|
|
6
|
+
export declare function slugify(name: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* `slugify` with a fallback, so a name written entirely in a non-latin script still yields a
|
|
9
|
+
* usable folder instead of an empty one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function slugifyOr(name: string, fallback: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* The first claimant of a slug keeps it; the next ones get `-2`, `-3`… Callers assign folders in a
|
|
14
|
+
* stable order (by app id), so the same team always produces the same layout — and once assigned,
|
|
15
|
+
* the folder is recorded in the team metadata and never derived again.
|
|
16
|
+
*/
|
|
17
|
+
export declare function uniqueSlug(slug: string, taken: Set<string>): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.slugify = slugify;
|
|
4
|
+
exports.slugifyOr = slugifyOr;
|
|
5
|
+
exports.uniqueSlug = uniqueSlug;
|
|
6
|
+
/**
|
|
7
|
+
* Folder name for a development team or an App, derived from its display name. Names are free
|
|
8
|
+
* text — accents, punctuation, emoji — while the folder has to be portable and typeable, so
|
|
9
|
+
* everything outside `[a-z0-9-]` collapses into a single dash.
|
|
10
|
+
*/
|
|
11
|
+
function slugify(name) {
|
|
12
|
+
return name
|
|
13
|
+
.normalize('NFKD')
|
|
14
|
+
.replaceAll(/[̀-ͯ]/g, '')
|
|
15
|
+
.replaceAll(/[^a-zA-Z\d]+/g, '-')
|
|
16
|
+
.replaceAll(/^-+|-+$/g, '')
|
|
17
|
+
.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `slugify` with a fallback, so a name written entirely in a non-latin script still yields a
|
|
21
|
+
* usable folder instead of an empty one.
|
|
22
|
+
*/
|
|
23
|
+
function slugifyOr(name, fallback) {
|
|
24
|
+
const slug = slugify(name);
|
|
25
|
+
return slug.length > 0 ? slug : fallback;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The first claimant of a slug keeps it; the next ones get `-2`, `-3`… Callers assign folders in a
|
|
29
|
+
* stable order (by app id), so the same team always produces the same layout — and once assigned,
|
|
30
|
+
* the folder is recorded in the team metadata and never derived again.
|
|
31
|
+
*/
|
|
32
|
+
function uniqueSlug(slug, taken) {
|
|
33
|
+
if (!taken.has(slug)) {
|
|
34
|
+
return slug;
|
|
35
|
+
}
|
|
36
|
+
let suffix = 2;
|
|
37
|
+
while (taken.has(`${slug}-${suffix}`)) {
|
|
38
|
+
suffix += 1;
|
|
39
|
+
}
|
|
40
|
+
return `${slug}-${suffix}`;
|
|
41
|
+
}
|