@factorialco/fcode-cli 2.4.0 → 3.0.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 +331 -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 +91 -0
- package/dist/commands/team/pull.d.ts +3 -7
- package/dist/commands/team/pull.js +25 -30
- package/dist/commands/team/status.d.ts +3 -4
- package/dist/commands/team/status.js +34 -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 +48 -0
- package/dist/config/dev-team.js +62 -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 +31 -0
- package/dist/services/dashboard-client.js +99 -0
- package/dist/services/dev-team-service.d.ts +41 -0
- package/dist/services/dev-team-service.js +162 -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,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planAppSync = planAppSync;
|
|
4
|
+
exports.assignFolder = assignFolder;
|
|
5
|
+
exports.syncApps = syncApps;
|
|
6
|
+
exports.reportOutcomes = reportOutcomes;
|
|
7
|
+
exports.resolveTeam = resolveTeam;
|
|
8
|
+
const fs = require("fs-extra");
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
const color_1 = require("@oclif/color");
|
|
11
|
+
const pull_1 = require("../commands/pull");
|
|
12
|
+
const dev_team_1 = require("../config/dev-team");
|
|
13
|
+
const skills_installer_1 = require("./skills-installer");
|
|
14
|
+
const dev_slug_1 = require("../utils/dev-slug");
|
|
15
|
+
const workspace_cloner_1 = require("./workspace-cloner");
|
|
16
|
+
const slugify_1 = require("../utils/slugify");
|
|
17
|
+
const cli_1 = require("../utils/cli");
|
|
18
|
+
function planAppSync(metadata, apps) {
|
|
19
|
+
const remoteIds = new Set(apps.map(({ id }) => id));
|
|
20
|
+
return {
|
|
21
|
+
toClone: apps.filter(({ id }) => metadata.apps[id] === undefined),
|
|
22
|
+
toRefresh: apps.filter(({ id }) => metadata.apps[id] !== undefined),
|
|
23
|
+
removed: Object.entries(metadata.apps)
|
|
24
|
+
.filter(([id]) => !remoteIds.has(id))
|
|
25
|
+
.map(([id, app]) => ({ id, app })),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The folder an App gets inside the team root. Names are free text and two Apps may well slugify
|
|
30
|
+
* the same, so the first claimant keeps the plain slug and the next ones are suffixed. Only ever
|
|
31
|
+
* called for an App without a recorded folder — afterwards the team metadata is authoritative, so
|
|
32
|
+
* a renamed App keeps the folder it was cloned into.
|
|
33
|
+
*/
|
|
34
|
+
function assignFolder(app, metadata) {
|
|
35
|
+
const taken = new Set(Object.values(metadata.apps).map(({ folder }) => folder));
|
|
36
|
+
return (0, slugify_1.uniqueSlug)((0, slugify_1.slugifyOr)(app.name, app.id), taken);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The team folder holds the skills once (installing them per App would download the same tree N
|
|
40
|
+
* times); the App and its workspace link to them, so an agent started in either place resolves
|
|
41
|
+
* them. The link stays out of git through the App's own .gitignore.
|
|
42
|
+
*/
|
|
43
|
+
function linkSkills(rootDir, appDir, workspaceDir) {
|
|
44
|
+
(0, skills_installer_1.linkSkillsInto)(appDir, rootDir);
|
|
45
|
+
(0, skills_installer_1.linkSkillsInto)(workspaceDir, rootDir);
|
|
46
|
+
}
|
|
47
|
+
/** A committed .gitignore, since each App folder is meant to become its own repository. */
|
|
48
|
+
function writeAppGitIgnore(appDir) {
|
|
49
|
+
const gitIgnorePath = path.join(appDir, '.gitignore');
|
|
50
|
+
if (fs.pathExistsSync(gitIgnorePath)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
fs.outputFileSync(gitIgnorePath, ['# Agent skills are installed once at the development-team root and linked here.', '**/.claude/skills', ''].join('\n'));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Clones one App. The metadata entry is recorded as soon as the directory exists, before the
|
|
57
|
+
* pull that may fail: a half-cloned App has to be recognised as cloned next time, or the retry
|
|
58
|
+
* would try to create it again and stop at "already cloned".
|
|
59
|
+
*/
|
|
60
|
+
async function cloneApp(rootDir, app, folder, metadata) {
|
|
61
|
+
const slug = (0, dev_slug_1.devSlug)(app.id);
|
|
62
|
+
const appDir = path.join(rootDir, folder);
|
|
63
|
+
const workspaceDir = path.join(appDir, dev_team_1.APP_WORKSPACE_DIR);
|
|
64
|
+
cli_1.default.log(color_1.default.cyan(`\n📦 ${app.name} → ${folder}/${dev_team_1.APP_WORKSPACE_DIR} (${slug})`));
|
|
65
|
+
await (0, workspace_cloner_1.scaffoldWorkspace)(workspaceDir, { url: metadata.cloudUrl, slug });
|
|
66
|
+
metadata.apps[app.id] = { name: app.name, description: app.description, folder, devWorkspaceSlug: slug };
|
|
67
|
+
(0, dev_team_1.writeAppSettings)(appDir, { id: app.id, name: app.name, description: app.description });
|
|
68
|
+
writeAppGitIgnore(appDir);
|
|
69
|
+
linkSkills(rootDir, appDir, workspaceDir);
|
|
70
|
+
await (0, workspace_cloner_1.runInWorkspace)(workspaceDir, () => pull_1.default.run([]));
|
|
71
|
+
}
|
|
72
|
+
async function refreshApp(rootDir, cloudUrl, app, recorded) {
|
|
73
|
+
const appDir = path.join(rootDir, recorded.folder);
|
|
74
|
+
const workspaceDir = path.join(appDir, dev_team_1.APP_WORKSPACE_DIR);
|
|
75
|
+
cli_1.default.log(color_1.default.cyan(`\n📦 ${app.name} → ${recorded.folder}/${dev_team_1.APP_WORKSPACE_DIR}`));
|
|
76
|
+
// A recorded App whose workspace is gone — a failed first clone, or a directory the user
|
|
77
|
+
// removed — is laid out again rather than failing every pull from here on.
|
|
78
|
+
if (!fs.pathExistsSync(path.join(workspaceDir, '.fcode'))) {
|
|
79
|
+
await (0, workspace_cloner_1.scaffoldWorkspace)(workspaceDir, { url: cloudUrl, slug: recorded.devWorkspaceSlug });
|
|
80
|
+
}
|
|
81
|
+
// Name and description live in the dashboard; the local copy is a mirror refreshed on every
|
|
82
|
+
// pull. There is deliberately no team:push — pushing happens inside a workspace.
|
|
83
|
+
(0, dev_team_1.writeAppSettings)(appDir, { id: app.id, name: app.name, description: app.description });
|
|
84
|
+
writeAppGitIgnore(appDir);
|
|
85
|
+
recorded.name = app.name;
|
|
86
|
+
recorded.description = app.description;
|
|
87
|
+
linkSkills(rootDir, appDir, workspaceDir);
|
|
88
|
+
await (0, workspace_cloner_1.runInWorkspace)(workspaceDir, () => pull_1.default.run([]));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Applies the plan, one App at a time, recording the metadata of each App that lands. A failing
|
|
92
|
+
* App is reported and the run carries on: with a dozen Apps, one bad workspace must not cost the
|
|
93
|
+
* other eleven.
|
|
94
|
+
*/
|
|
95
|
+
async function syncApps(rootDir, metadata, plan) {
|
|
96
|
+
const outcomes = [];
|
|
97
|
+
for (const app of plan.toClone) {
|
|
98
|
+
const folder = assignFolder(app, metadata);
|
|
99
|
+
try {
|
|
100
|
+
await cloneApp(rootDir, app, folder, metadata);
|
|
101
|
+
outcomes.push({ name: app.name, folder, action: 'cloned' });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
outcomes.push({ name: app.name, folder, action: 'cloned', error: error });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const app of plan.toRefresh) {
|
|
108
|
+
const recorded = metadata.apps[app.id];
|
|
109
|
+
try {
|
|
110
|
+
await refreshApp(rootDir, metadata.cloudUrl, app, recorded);
|
|
111
|
+
outcomes.push({ name: app.name, folder: recorded.folder, action: 'pulled' });
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
outcomes.push({ name: app.name, folder: recorded.folder, action: 'pulled', error: error });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return outcomes;
|
|
118
|
+
}
|
|
119
|
+
/** Prints the per-App result and reports whether anything failed. */
|
|
120
|
+
function reportOutcomes(outcomes, removed) {
|
|
121
|
+
cli_1.default.log('');
|
|
122
|
+
for (const outcome of outcomes) {
|
|
123
|
+
if (outcome.error) {
|
|
124
|
+
cli_1.default.log(color_1.default.red(`❌ ${outcome.folder} — ${outcome.error.message}`));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
cli_1.default.log(color_1.default.green(`✅ ${outcome.folder} (${outcome.action})`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const { app } of removed) {
|
|
131
|
+
cli_1.default.log(color_1.default.yellow(`⚠️ ${app.folder} is no longer in the team — the folder was kept, remove it yourself`));
|
|
132
|
+
}
|
|
133
|
+
return outcomes.some(({ error }) => error !== undefined);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The team to operate on: the one named, the only one the session belongs to, or — when there are
|
|
137
|
+
* several — none, after listing them so the user can pass an id.
|
|
138
|
+
*/
|
|
139
|
+
async function resolveTeam(client, teamId) {
|
|
140
|
+
const teams = await client.listDevelopmentTeams();
|
|
141
|
+
if (teamId) {
|
|
142
|
+
const team = teams.find(({ id }) => id === teamId);
|
|
143
|
+
if (!team) {
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
cli_1.default.error(`Development team ${teamId} not found, or your session has no access to it`);
|
|
146
|
+
}
|
|
147
|
+
return team;
|
|
148
|
+
}
|
|
149
|
+
if (teams.length === 0) {
|
|
150
|
+
cli_1.default.log('ℹ️ Your session belongs to no development team. Ask your team admin to add you.');
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
if (teams.length === 1) {
|
|
154
|
+
return teams[0];
|
|
155
|
+
}
|
|
156
|
+
cli_1.default.log('🏢 Your development teams:\n');
|
|
157
|
+
for (const team of teams) {
|
|
158
|
+
cli_1.default.log(` ${color_1.default.cyan(team.id)} ${team.name}`);
|
|
159
|
+
}
|
|
160
|
+
cli_1.default.log(`\nClone one with ${color_1.default.cyan('fcode team:clone [teamId]')}`);
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GraphQLClient } from 'graphql-request';
|
|
2
|
-
import { Locale, Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, TeamVersion, TeamVersionAlias, Variable, VersionedProcess, VersionedScriptLibrary, ServiceAccount } from '../core/types';
|
|
2
|
+
import { Locale, Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, TeamVersion, TeamVersionAlias, Variable, VersionedProcess, VersionedProgrammingLanguageManifest, VersionedScriptLibrary, ServiceAccount } from '../core/types';
|
|
3
3
|
export type FcodeGraphQLClientOptions = {
|
|
4
4
|
url: string;
|
|
5
5
|
accessToken: string;
|
|
@@ -50,7 +50,7 @@ declare class FcodeGraphQLClient {
|
|
|
50
50
|
private mapWebhook;
|
|
51
51
|
mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug }: any): Process;
|
|
52
52
|
/**
|
|
53
|
-
* GraphQL nests the webhook configuration under `settings`, while
|
|
53
|
+
* GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
|
|
54
54
|
* keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
|
|
55
55
|
* undefined.
|
|
56
56
|
*/
|
|
@@ -69,6 +69,20 @@ declare class FcodeGraphQLClient {
|
|
|
69
69
|
pushTeam(update: TeamUpdateInput): Promise<RawTeamSettings>;
|
|
70
70
|
mapProgrammingLanguageManifest(programmingLanguageManifest: any): ProgrammingLanguageManifest;
|
|
71
71
|
private mapDependencies;
|
|
72
|
+
/**
|
|
73
|
+
* The published dependency versions of one language, newest first. An fcode-api that predates
|
|
74
|
+
* dependency versioning rejects the query over the unknown field; that answers `null` — "the api
|
|
75
|
+
* cannot say" — so the pull skips versions entirely instead of reconciling (and deleting) local
|
|
76
|
+
* snapshot folders against an empty list. Only that schema-validation rejection is swallowed: a
|
|
77
|
+
* runtime failure of the field also mentions its name, and must fail the pull.
|
|
78
|
+
*/
|
|
79
|
+
allVersionedDependencies(programmingLanguage: ProgrammingLanguage): Promise<Array<VersionedProgrammingLanguageManifest> | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Whether the api rejected the query because it does not know {@code field} — as opposed to
|
|
82
|
+
* knowing it and failing to resolve it, whose message also contains the field name (graphql-java
|
|
83
|
+
* puts the path into runtime messages: "Exception while fetching data (/field) : ...").
|
|
84
|
+
*/
|
|
85
|
+
private isUndefinedFieldError;
|
|
72
86
|
allProgrammingLanguageManifest(): Promise<Array<ProgrammingLanguageManifest>>;
|
|
73
87
|
pushProgrammingLanguageManifest(programmingLanguageManifest: ProgrammingLanguageManifest): Promise<ProgrammingLanguageManifest>;
|
|
74
88
|
allServiceAccounts(): Promise<Array<ServiceAccount>>;
|
|
@@ -362,9 +362,9 @@ class FcodeGraphQLClient {
|
|
|
362
362
|
return Promise.reject(this.cleanError(error));
|
|
363
363
|
}
|
|
364
364
|
// The cloud team's parent order is the inheritance precedence the merge must replicate; the
|
|
365
|
-
// local
|
|
366
|
-
const {
|
|
367
|
-
const merged = (0, locale_merger_1.mergeInheritedLocales)(locales,
|
|
365
|
+
// local settings.json may lag behind it, so the cloud is asked, not the file.
|
|
366
|
+
const { parentTeams } = await this.team();
|
|
367
|
+
const merged = (0, locale_merger_1.mergeInheritedLocales)(locales, (parentTeams ?? []).map(({ slug }) => slug));
|
|
368
368
|
(0, locale_merger_1.assertLocaleFilesCanCoexist)(merged);
|
|
369
369
|
return merged;
|
|
370
370
|
}
|
|
@@ -786,7 +786,7 @@ class FcodeGraphQLClient {
|
|
|
786
786
|
return new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug));
|
|
787
787
|
}
|
|
788
788
|
/**
|
|
789
|
-
* GraphQL nests the webhook configuration under `settings`, while
|
|
789
|
+
* GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
|
|
790
790
|
* keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
|
|
791
791
|
* undefined.
|
|
792
792
|
*/
|
|
@@ -795,7 +795,7 @@ class FcodeGraphQLClient {
|
|
|
795
795
|
slug: team.slug,
|
|
796
796
|
zoneId: team.zoneId,
|
|
797
797
|
errorHandlerConfig: team.errorHandlerConfig,
|
|
798
|
-
|
|
798
|
+
parentTeams: team.parentTeams ?? [],
|
|
799
799
|
};
|
|
800
800
|
const webhookAuth = team.settings?.webhookAuth;
|
|
801
801
|
if (webhookAuth?.variableKey) {
|
|
@@ -947,7 +947,10 @@ class FcodeGraphQLClient {
|
|
|
947
947
|
processId
|
|
948
948
|
tag
|
|
949
949
|
}
|
|
950
|
-
|
|
950
|
+
parentTeams {
|
|
951
|
+
slug
|
|
952
|
+
version
|
|
953
|
+
}
|
|
951
954
|
settings {
|
|
952
955
|
webhookAuth {
|
|
953
956
|
headerName
|
|
@@ -974,7 +977,10 @@ class FcodeGraphQLClient {
|
|
|
974
977
|
processId
|
|
975
978
|
tag
|
|
976
979
|
}
|
|
977
|
-
|
|
980
|
+
parentTeams {
|
|
981
|
+
slug
|
|
982
|
+
version
|
|
983
|
+
}
|
|
978
984
|
settings {
|
|
979
985
|
webhookAuth {
|
|
980
986
|
headerName
|
|
@@ -1010,6 +1016,49 @@ class FcodeGraphQLClient {
|
|
|
1010
1016
|
.filter(Boolean)
|
|
1011
1017
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1012
1018
|
}
|
|
1019
|
+
/**
|
|
1020
|
+
* The published dependency versions of one language, newest first. An fcode-api that predates
|
|
1021
|
+
* dependency versioning rejects the query over the unknown field; that answers `null` — "the api
|
|
1022
|
+
* cannot say" — so the pull skips versions entirely instead of reconciling (and deleting) local
|
|
1023
|
+
* snapshot folders against an empty list. Only that schema-validation rejection is swallowed: a
|
|
1024
|
+
* runtime failure of the field also mentions its name, and must fail the pull.
|
|
1025
|
+
*/
|
|
1026
|
+
allVersionedDependencies(programmingLanguage) {
|
|
1027
|
+
return this.client
|
|
1028
|
+
.request((0, graphql_request_1.gql) `
|
|
1029
|
+
query allVersionedDependencies($programmingLanguage: ProgrammingLanguage!) {
|
|
1030
|
+
allVersionedDependencies(programmingLanguage: $programmingLanguage) {
|
|
1031
|
+
tag
|
|
1032
|
+
programmingLanguage
|
|
1033
|
+
dependencies {
|
|
1034
|
+
name
|
|
1035
|
+
semVer
|
|
1036
|
+
isNative
|
|
1037
|
+
gitUrl
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
`, { programmingLanguage })
|
|
1042
|
+
.then(({ allVersionedDependencies }) => allVersionedDependencies.map((versioned) => new types_1.VersionedProgrammingLanguageManifest(versioned.tag, versioned.programmingLanguage, this.mapDependencies(versioned.dependencies))))
|
|
1043
|
+
.catch((error) => {
|
|
1044
|
+
if (this.isUndefinedFieldError(error, 'allVersionedDependencies')) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
return Promise.reject(this.cleanError(error));
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Whether the api rejected the query because it does not know {@code field} — as opposed to
|
|
1052
|
+
* knowing it and failing to resolve it, whose message also contains the field name (graphql-java
|
|
1053
|
+
* puts the path into runtime messages: "Exception while fetching data (/field) : ...").
|
|
1054
|
+
*/
|
|
1055
|
+
isUndefinedFieldError(error, field) {
|
|
1056
|
+
const first = error.response?.errors?.[0];
|
|
1057
|
+
if (!first?.message?.includes(field)) {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
return first.extensions?.classification === 'ValidationError' || first.message.startsWith('Validation error');
|
|
1061
|
+
}
|
|
1013
1062
|
allProgrammingLanguageManifest() {
|
|
1014
1063
|
const result = [];
|
|
1015
1064
|
result.push(this.programmingLanguageManifest('JAVASCRIPT'));
|
|
@@ -8,13 +8,24 @@
|
|
|
8
8
|
* ones, which `pull` writes into the same directory as `<locale>.inherited.yaml`.
|
|
9
9
|
*/
|
|
10
10
|
export declare function flattenTranslations(document: unknown, prefix?: string, target?: Record<string, string>): Record<string, string>;
|
|
11
|
+
/**
|
|
12
|
+
* The locale files that answer a requested identifier, lowest precedence first. Mirrors the
|
|
13
|
+
* platform's `LocaleFallbackChain`, so a local run and the cloud resolve the same text.
|
|
14
|
+
*
|
|
15
|
+
* A viewer's locale is usually a regional variant (`en-gb`, `pt-BR`) while a workspace usually
|
|
16
|
+
* publishes plain languages, so the request degrades one segment at a time — split on `-` or `_`,
|
|
17
|
+
* without parsing a language tag, since any short slug is a legal identifier here — and every step
|
|
18
|
+
* that matches stays in the chain: a variant file translates the keys that differ and leaves the
|
|
19
|
+
* rest to the language it came from.
|
|
20
|
+
*/
|
|
21
|
+
export declare function localeFallbackChain(requested: string | undefined, primary: string | undefined, available: Array<string>): Array<string>;
|
|
11
22
|
/**
|
|
12
23
|
* @param requestedLocale the locale to run with, or undefined to use the workspace's primary
|
|
13
|
-
* @returns the locale actually used and its flat translations, with the primary locale
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* translations
|
|
24
|
+
* @returns the locale actually used and its flat translations, with the primary locale — and any
|
|
25
|
+
* shorter form of the requested one the workspace publishes — layered underneath, plus each
|
|
26
|
+
* locale's own map (inherited file layered under its own, but NOT merged over the primary) so a
|
|
27
|
+
* call naming a locale (`fcode.i18n(key, args, { locale })`) resolves like the cloud's `by_locale`:
|
|
28
|
+
* key by key in the requested locale first, falling back to the merged translations
|
|
18
29
|
*/
|
|
19
30
|
export declare function resolveLocalTranslations(requestedLocale?: string, workspaceDir?: string): {
|
|
20
31
|
locale?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.flattenTranslations = flattenTranslations;
|
|
4
|
+
exports.localeFallbackChain = localeFallbackChain;
|
|
4
5
|
exports.resolveLocalTranslations = resolveLocalTranslations;
|
|
5
6
|
const fs = require("fs-extra");
|
|
6
7
|
const path = require("node:path");
|
|
@@ -8,6 +9,7 @@ const yaml_1 = require("yaml");
|
|
|
8
9
|
const cli_1 = require("../utils/cli");
|
|
9
10
|
const color_1 = require("@oclif/color");
|
|
10
11
|
const types_1 = require("../core/types");
|
|
12
|
+
const settings_1 = require("../repositories/settings");
|
|
11
13
|
const I18N_DIRECTORY = 'i18n';
|
|
12
14
|
const YAML_EXTENSION = '.yaml';
|
|
13
15
|
const KEY_SEPARATOR = '.';
|
|
@@ -71,8 +73,9 @@ function readLocale(workspaceDir, locale) {
|
|
|
71
73
|
}
|
|
72
74
|
function chosenPrimaryLocaleOf(workspaceDir) {
|
|
73
75
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
(0, settings_1.migrateLegacySettingsFile)(workspaceDir);
|
|
77
|
+
const settings = fs.readJsonSync(path.join(workspaceDir, settings_1.SETTINGS_FILENAME));
|
|
78
|
+
return typeof settings?.primaryLocale === 'string' && settings.primaryLocale.length > 0 ? settings.primaryLocale : undefined;
|
|
76
79
|
}
|
|
77
80
|
catch {
|
|
78
81
|
return undefined;
|
|
@@ -95,7 +98,7 @@ function availableLocales(workspaceDir) {
|
|
|
95
98
|
return [...new Set(localeFileSlugs(workspaceDir).map((slug) => identifierOf(slug)))].sort();
|
|
96
99
|
}
|
|
97
100
|
/**
|
|
98
|
-
* The workspace's primary locale: the one chosen in `
|
|
101
|
+
* The workspace's primary locale: the one chosen in `settings.json`, or its first locale when none is
|
|
99
102
|
* chosen — or when the chosen one has no file anymore, which would otherwise resolve zero keys.
|
|
100
103
|
* Mirrors the platform's `PrimaryLocale.resolve`, so a local run and the cloud agree on which
|
|
101
104
|
* locale a process without an explicit one resolves against.
|
|
@@ -105,13 +108,55 @@ function primaryLocaleOf(workspaceDir) {
|
|
|
105
108
|
const chosen = chosenPrimaryLocaleOf(workspaceDir);
|
|
106
109
|
return chosen && available.includes(chosen) ? chosen : available[0];
|
|
107
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The available locale a candidate opens: the one named exactly, or — only when no name matches —
|
|
113
|
+
* the one that differs from it in case alone, alphabetically first if there are several. Locale
|
|
114
|
+
* identifiers are case-sensitive, so `pt-BR` and `pt-br` are two files and each keeps answering its
|
|
115
|
+
* own name; ignoring case only ever rescues a lookup that would otherwise find nothing.
|
|
116
|
+
*/
|
|
117
|
+
function matchingLocale(candidate, available) {
|
|
118
|
+
if (available.includes(candidate)) {
|
|
119
|
+
return candidate;
|
|
120
|
+
}
|
|
121
|
+
return available.filter((locale) => locale.toLowerCase() === candidate.toLowerCase()).sort()[0];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The locale files that answer a requested identifier, lowest precedence first. Mirrors the
|
|
125
|
+
* platform's `LocaleFallbackChain`, so a local run and the cloud resolve the same text.
|
|
126
|
+
*
|
|
127
|
+
* A viewer's locale is usually a regional variant (`en-gb`, `pt-BR`) while a workspace usually
|
|
128
|
+
* publishes plain languages, so the request degrades one segment at a time — split on `-` or `_`,
|
|
129
|
+
* without parsing a language tag, since any short slug is a legal identifier here — and every step
|
|
130
|
+
* that matches stays in the chain: a variant file translates the keys that differ and leaves the
|
|
131
|
+
* rest to the language it came from.
|
|
132
|
+
*/
|
|
133
|
+
function localeFallbackChain(requested, primary, available) {
|
|
134
|
+
const chain = [];
|
|
135
|
+
const add = (candidate) => {
|
|
136
|
+
const match = candidate ? matchingLocale(candidate, available) : undefined;
|
|
137
|
+
if (match && !chain.includes(match)) {
|
|
138
|
+
chain.push(match);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
add(primary);
|
|
142
|
+
const candidates = [];
|
|
143
|
+
let candidate = requested?.trim();
|
|
144
|
+
while (candidate) {
|
|
145
|
+
candidates.unshift(candidate);
|
|
146
|
+
const lastSeparator = Math.max(candidate.lastIndexOf('-'), candidate.lastIndexOf('_'));
|
|
147
|
+
candidate = lastSeparator > 0 ? candidate.slice(0, lastSeparator) : undefined;
|
|
148
|
+
}
|
|
149
|
+
for (const shorterOrExact of candidates)
|
|
150
|
+
add(shorterOrExact);
|
|
151
|
+
return chain;
|
|
152
|
+
}
|
|
108
153
|
/**
|
|
109
154
|
* @param requestedLocale the locale to run with, or undefined to use the workspace's primary
|
|
110
|
-
* @returns the locale actually used and its flat translations, with the primary locale
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* translations
|
|
155
|
+
* @returns the locale actually used and its flat translations, with the primary locale — and any
|
|
156
|
+
* shorter form of the requested one the workspace publishes — layered underneath, plus each
|
|
157
|
+
* locale's own map (inherited file layered under its own, but NOT merged over the primary) so a
|
|
158
|
+
* call naming a locale (`fcode.i18n(key, args, { locale })`) resolves like the cloud's `by_locale`:
|
|
159
|
+
* key by key in the requested locale first, falling back to the merged translations
|
|
115
160
|
*/
|
|
116
161
|
function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd()) {
|
|
117
162
|
const primary = primaryLocaleOf(workspaceDir);
|
|
@@ -120,8 +165,11 @@ function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd())
|
|
|
120
165
|
if (!locale) {
|
|
121
166
|
return { locale: undefined, translations: {}, byLocale };
|
|
122
167
|
}
|
|
123
|
-
// `byLocale` already holds every locale's own file, so the merged map — the
|
|
124
|
-
// layered
|
|
125
|
-
const
|
|
126
|
-
|
|
168
|
+
// `byLocale` already holds every locale's own file, so the merged map — the fallback chain
|
|
169
|
+
// layered lowest precedence first — is derived from it rather than read from disk a second time.
|
|
170
|
+
const translations = {};
|
|
171
|
+
for (const layer of localeFallbackChain(requestedLocale, primary, Object.keys(byLocale))) {
|
|
172
|
+
Object.assign(translations, byLocale[layer]);
|
|
173
|
+
}
|
|
174
|
+
return { locale, translations, byLocale };
|
|
127
175
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Locale, Process, ProgrammingLanguageManifest, Resource, ResourceStatus, ScriptLibrary, Variable } from '../core/types';
|
|
2
|
+
import { PseudoFile } from '../utils/diff';
|
|
3
|
+
/** Stands in for a variable's value wherever one side declares it sensitive, so no secret is ever printed. */
|
|
4
|
+
export declare const SENSITIVE_PLACEHOLDER = "<sensitive value hidden>";
|
|
5
|
+
export declare const VARIABLES_DIFF_PATH = "variables.env";
|
|
6
|
+
/** What a workspace inherits is regenerated on pull into its own file, which the parent owns. */
|
|
7
|
+
export declare const INHERITED_VARIABLES_DIFF_PATH = "variables.inherited.env";
|
|
8
|
+
export declare const SETTINGS_DIFF_PATH = "settings.json";
|
|
9
|
+
/**
|
|
10
|
+
* A resource as both sides hold it, kept whole. `ResourceMerger.mergeStatus` collapses the two into
|
|
11
|
+
* one object and loses the remote body, which is exactly what a diff needs.
|
|
12
|
+
*/
|
|
13
|
+
export type ResourcePair<R extends Resource> = {
|
|
14
|
+
slug: string;
|
|
15
|
+
remote?: R;
|
|
16
|
+
local?: R;
|
|
17
|
+
status: ResourceStatus;
|
|
18
|
+
};
|
|
19
|
+
/** A set of pseudo-files presented together under one heading (usually one resource). */
|
|
20
|
+
export type DiffSection = {
|
|
21
|
+
label: string;
|
|
22
|
+
files: PseudoFile[];
|
|
23
|
+
};
|
|
24
|
+
/** Files keyed by the path they are stored at, for one side of a comparison. */
|
|
25
|
+
type SideFiles = Record<string, string>;
|
|
26
|
+
export declare function pairResources<R extends Resource>(remoteResources: Array<R>, localResources: Array<R>, existsMetadata: (slug: string) => Promise<boolean>): Promise<Array<ResourcePair<R>>>;
|
|
27
|
+
/**
|
|
28
|
+
* The workspace a resource is inherited from, when it is. An overridden variable resolves to
|
|
29
|
+
* undefined on its local side, which is what makes an override diffable while the parent's copy
|
|
30
|
+
* stays read-only.
|
|
31
|
+
*/
|
|
32
|
+
export declare function inheritedFrom<R extends Resource>(pair: ResourcePair<R>): string | undefined;
|
|
33
|
+
/** A process as the four files it is stored as, plus its own dependencies file when it scopes them. */
|
|
34
|
+
export declare function processFiles(process: Process): SideFiles;
|
|
35
|
+
export declare function moduleFiles(module: ScriptLibrary): SideFiles;
|
|
36
|
+
export declare function localeFiles(locale: Locale): SideFiles;
|
|
37
|
+
/**
|
|
38
|
+
* Only the workspace's own dependencies, matching what `contentHash` covers and what the manifest
|
|
39
|
+
* file holds: the inherited ones live in their own file and belong to a parent.
|
|
40
|
+
*/
|
|
41
|
+
export declare function manifestFiles(manifest: ProgrammingLanguageManifest): SideFiles;
|
|
42
|
+
export declare const processDiffFiles: (pair: ResourcePair<Process>) => Array<PseudoFile>;
|
|
43
|
+
export declare const moduleDiffFiles: (pair: ResourcePair<ScriptLibrary>) => Array<PseudoFile>;
|
|
44
|
+
export declare const localeDiffFiles: (pair: ResourcePair<Locale>) => Array<PseudoFile>;
|
|
45
|
+
export declare const manifestDiffFiles: (pair: ResourcePair<ProgrammingLanguageManifest>) => Array<PseudoFile>;
|
|
46
|
+
/**
|
|
47
|
+
* Every variable as the .env files they share, rather than one pseudo-file each: that is how they
|
|
48
|
+
* are stored, and it keeps a rename or a reordering readable as one patch.
|
|
49
|
+
*
|
|
50
|
+
* The workspace's own keys and the inherited ones are separate files, because they are separate
|
|
51
|
+
* files on disk — a drifted inherited variable is a stale `variables.inherited.env` that a pull
|
|
52
|
+
* regenerates, not something to push. The inherited file only appears when something is inherited.
|
|
53
|
+
*
|
|
54
|
+
* A key either side marks sensitive is rendered as a placeholder on both, so a diff never prints a
|
|
55
|
+
* secret and a value-only change to a sensitive variable shows up as nothing (its status does).
|
|
56
|
+
*/
|
|
57
|
+
export declare function variablesEnvDiffFiles(pairs: Array<ResourcePair<Variable>>): Array<PseudoFile>;
|
|
58
|
+
/** The singleton workspace settings file. Both sides arrive already serialized by `SettingsRepository.toFile`. */
|
|
59
|
+
export declare function settingsDiffFile(remote: Record<string, any> | undefined, local: Record<string, any> | undefined): PseudoFile;
|
|
60
|
+
export {};
|
|
@@ -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
|
+
}
|