@factorialco/fcode-cli 2.3.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 +334 -117
- 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/dependencies/status.d.ts +11 -0
- package/dist/commands/dependencies/status.js +43 -0
- 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 +3 -3
- 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/{team/push.d.ts → settings/pull.d.ts} +3 -3
- package/dist/commands/settings/pull.js +41 -0
- package/dist/commands/settings/push.d.ts +13 -0
- package/dist/commands/settings/push.js +103 -0
- 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 -2
- package/dist/commands/settings/versions/create.js +36 -0
- 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 -4
- package/dist/commands/team/pull.js +25 -19
- package/dist/commands/team/status.d.ts +3 -4
- package/dist/commands/team/status.js +34 -27
- 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/dependencies/env-dependencies-command.js +6 -3
- package/dist/core/executors/javascript-execution-task.js +36 -5
- package/dist/core/types.d.ts +59 -9
- package/dist/core/types.js +64 -10
- package/dist/core/utils/dependencies.js +4 -1
- package/dist/env.js +1 -1
- package/dist/repositories/process.d.ts +8 -2
- package/dist/repositories/process.js +29 -15
- package/dist/repositories/programming-language-manifest.d.ts +31 -1
- package/dist/repositories/programming-language-manifest.js +96 -2
- package/dist/repositories/settings.d.ts +52 -0
- package/dist/repositories/settings.js +214 -0
- package/dist/runtime/libs/python/fcode/context.py +42 -8
- 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 +17 -2
- package/dist/services/fcode-graphql-client.js +127 -15
- package/dist/services/locale-resolver.d.ts +17 -1
- package/dist/services/locale-resolver.js +62 -7
- 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 +16 -0
- package/dist/services/team-versioning.js +40 -0
- 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/gitignore.d.ts +3 -1
- package/dist/utils/gitignore.js +11 -2
- package/dist/utils/slugify.d.ts +17 -0
- package/dist/utils/slugify.js +41 -0
- package/oclif.manifest.json +433 -113
- package/package.json +12 -7
- package/dist/commands/team/push.js +0 -52
- package/dist/commands/team/versions/create.js +0 -42
- package/dist/repositories/team.d.ts +0 -31
- package/dist/repositories/team.js +0 -127
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import GlobalConfig from '../config/global';
|
|
2
|
+
export interface DevelopmentTeam {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DevelopmentTeamApp {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `https://code.factorialhr.com/platform` → `https://code.factorialhr.com/dashboard/api/v1`.
|
|
13
|
+
* The dashboard is served under `/dashboard` on the same origin as the platform; environments
|
|
14
|
+
* that split them can point `FCODE_DASHBOARD_URL` at the API base directly.
|
|
15
|
+
*/
|
|
16
|
+
export declare function dashboardApiUrl(cloudUrl: string): string;
|
|
17
|
+
export default class DashboardClient {
|
|
18
|
+
private readonly cloudUrl;
|
|
19
|
+
private readonly baseUrl;
|
|
20
|
+
/** Instance property so tests can supply a stub instead of the real `~/.fcode/fcode.json`. */
|
|
21
|
+
globalConfig: GlobalConfig;
|
|
22
|
+
constructor(cloudUrl: string);
|
|
23
|
+
/** The development teams the session belongs to. Operators get every team. */
|
|
24
|
+
listDevelopmentTeams(): Promise<DevelopmentTeam[]>;
|
|
25
|
+
/** The Apps owned by a development team, narrowed by the API to the ones the session can see. */
|
|
26
|
+
listTeamApps(teamId: string): Promise<DevelopmentTeamApp[]>;
|
|
27
|
+
private getAllPages;
|
|
28
|
+
private get;
|
|
29
|
+
private headers;
|
|
30
|
+
private errorDetail;
|
|
31
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dashboardApiUrl = dashboardApiUrl;
|
|
4
|
+
const node_fetch_1 = require("node-fetch");
|
|
5
|
+
const auth_service_1 = require("./auth-service");
|
|
6
|
+
const global_1 = require("../config/global");
|
|
7
|
+
/**
|
|
8
|
+
* The dashboard's control plane — where Apps and development teams live — as opposed to the
|
|
9
|
+
* platform GraphQL API that `FcodeGraphQLClient` talks to. Both sit behind the same host and
|
|
10
|
+
* accept the same Keycloak token: the dashboard verifies the realm signature and issuer, so the
|
|
11
|
+
* session `fcode login` already established is enough.
|
|
12
|
+
*/
|
|
13
|
+
/** Max the dashboard's list endpoints accept (`listQuerySchema`), so a team is one round trip. */
|
|
14
|
+
const PAGE_SIZE = 200;
|
|
15
|
+
/**
|
|
16
|
+
* `https://code.factorialhr.com/platform` → `https://code.factorialhr.com/dashboard/api/v1`.
|
|
17
|
+
* The dashboard is served under `/dashboard` on the same origin as the platform; environments
|
|
18
|
+
* that split them can point `FCODE_DASHBOARD_URL` at the API base directly.
|
|
19
|
+
*/
|
|
20
|
+
function dashboardApiUrl(cloudUrl) {
|
|
21
|
+
const override = process.env.FCODE_DASHBOARD_URL;
|
|
22
|
+
if (override) {
|
|
23
|
+
return override.replace(/\/+$/, '');
|
|
24
|
+
}
|
|
25
|
+
return `${new URL(cloudUrl).origin}/dashboard/api/v1`;
|
|
26
|
+
}
|
|
27
|
+
class DashboardClient {
|
|
28
|
+
constructor(cloudUrl) {
|
|
29
|
+
this.cloudUrl = cloudUrl;
|
|
30
|
+
/** Instance property so tests can supply a stub instead of the real `~/.fcode/fcode.json`. */
|
|
31
|
+
this.globalConfig = new global_1.default();
|
|
32
|
+
this.baseUrl = dashboardApiUrl(cloudUrl);
|
|
33
|
+
}
|
|
34
|
+
/** The development teams the session belongs to. Operators get every team. */
|
|
35
|
+
async listDevelopmentTeams() {
|
|
36
|
+
const items = await this.getAllPages('/development-teams');
|
|
37
|
+
return items.map(({ id, company_name }) => ({ id, name: company_name }));
|
|
38
|
+
}
|
|
39
|
+
/** The Apps owned by a development team, narrowed by the API to the ones the session can see. */
|
|
40
|
+
async listTeamApps(teamId) {
|
|
41
|
+
const items = await this.getAllPages('/apps', {
|
|
42
|
+
development_team_id: teamId,
|
|
43
|
+
sort_by: 'name',
|
|
44
|
+
sort_dir: 'asc',
|
|
45
|
+
});
|
|
46
|
+
return items.map(({ id, name, description }) => ({ id, name, description }));
|
|
47
|
+
}
|
|
48
|
+
async getAllPages(path, query = {}) {
|
|
49
|
+
const items = [];
|
|
50
|
+
let offset = 0;
|
|
51
|
+
// `total` is authoritative rather than "a short page means the end": a page can come back
|
|
52
|
+
// short when the API narrows it to what the session may see.
|
|
53
|
+
let total = Number.POSITIVE_INFINITY;
|
|
54
|
+
while (items.length < total) {
|
|
55
|
+
const page = await this.get(path, {
|
|
56
|
+
...query,
|
|
57
|
+
limit: String(PAGE_SIZE),
|
|
58
|
+
offset: String(offset),
|
|
59
|
+
});
|
|
60
|
+
items.push(...page.items);
|
|
61
|
+
total = page.total;
|
|
62
|
+
if (page.items.length === 0) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
offset += page.items.length;
|
|
66
|
+
}
|
|
67
|
+
return items;
|
|
68
|
+
}
|
|
69
|
+
async get(path, query) {
|
|
70
|
+
const url = `${this.baseUrl}${path}?${new URLSearchParams(query).toString()}`;
|
|
71
|
+
let response = await (0, node_fetch_1.default)(url, { headers: await this.headers() });
|
|
72
|
+
// The token's group membership is snapshotted at issuance (FCT-61231), so a team joined after
|
|
73
|
+
// login reads as a denial. Re-mint once and retry before believing it.
|
|
74
|
+
if (response.status === 401 || response.status === 403) {
|
|
75
|
+
if (await auth_service_1.default.refreshSession(this.cloudUrl)) {
|
|
76
|
+
response = await (0, node_fetch_1.default)(url, { headers: await this.headers() });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Dashboard API ${response.status} on ${path}: ${await this.errorDetail(response)}`);
|
|
81
|
+
}
|
|
82
|
+
return (await response.json());
|
|
83
|
+
}
|
|
84
|
+
async headers() {
|
|
85
|
+
const { token } = await this.globalConfig.getAuthInfo(this.cloudUrl);
|
|
86
|
+
return { accept: 'application/json', authorization: `Bearer ${token.access_token}` };
|
|
87
|
+
}
|
|
88
|
+
async errorDetail(response) {
|
|
89
|
+
try {
|
|
90
|
+
const body = await response.text();
|
|
91
|
+
const parsed = JSON.parse(body);
|
|
92
|
+
return parsed.detail ?? parsed.message ?? body;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return 'no response body';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.default = DashboardClient;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import DashboardClient, { DevelopmentTeam, DevelopmentTeamApp } from './dashboard-client';
|
|
2
|
+
import { DevTeamApp, DevTeamMetadata } from '../config/dev-team';
|
|
3
|
+
/** What a team folder has to do to match the team as the dashboard currently describes it. */
|
|
4
|
+
export interface AppSyncPlan {
|
|
5
|
+
/** In the team, with no folder yet — cloned. */
|
|
6
|
+
toClone: DevelopmentTeamApp[];
|
|
7
|
+
/** In the team and already cloned — refreshed and pulled. */
|
|
8
|
+
toRefresh: DevelopmentTeamApp[];
|
|
9
|
+
/** Recorded locally but no longer in the team — reported, never deleted. */
|
|
10
|
+
removed: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
app: DevTeamApp;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
export declare function planAppSync(metadata: DevTeamMetadata, apps: DevelopmentTeamApp[]): AppSyncPlan;
|
|
16
|
+
/**
|
|
17
|
+
* The folder an App gets inside the team root. Names are free text and two Apps may well slugify
|
|
18
|
+
* the same, so the first claimant keeps the plain slug and the next ones are suffixed. Only ever
|
|
19
|
+
* called for an App without a recorded folder — afterwards the team metadata is authoritative, so
|
|
20
|
+
* a renamed App keeps the folder it was cloned into.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assignFolder(app: DevelopmentTeamApp, metadata: DevTeamMetadata): string;
|
|
23
|
+
export interface AppSyncOutcome {
|
|
24
|
+
name: string;
|
|
25
|
+
folder: string;
|
|
26
|
+
action: 'cloned' | 'pulled';
|
|
27
|
+
error?: Error;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Applies the plan, one App at a time, recording the metadata of each App that lands. A failing
|
|
31
|
+
* App is reported and the run carries on: with a dozen Apps, one bad workspace must not cost the
|
|
32
|
+
* other eleven.
|
|
33
|
+
*/
|
|
34
|
+
export declare function syncApps(rootDir: string, metadata: DevTeamMetadata, plan: AppSyncPlan): Promise<AppSyncOutcome[]>;
|
|
35
|
+
/** Prints the per-App result and reports whether anything failed. */
|
|
36
|
+
export declare function reportOutcomes(outcomes: AppSyncOutcome[], removed: AppSyncPlan['removed']): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The team to operate on: the one named, the only one the session belongs to, or — when there are
|
|
39
|
+
* several — none, after listing them so the user can pass an id.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveTeam(client: DashboardClient, teamId?: string): Promise<DevelopmentTeam | undefined>;
|
|
@@ -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
|
*/
|
|
@@ -68,6 +68,21 @@ declare class FcodeGraphQLClient {
|
|
|
68
68
|
team(): Promise<RawTeamSettings>;
|
|
69
69
|
pushTeam(update: TeamUpdateInput): Promise<RawTeamSettings>;
|
|
70
70
|
mapProgrammingLanguageManifest(programmingLanguageManifest: any): ProgrammingLanguageManifest;
|
|
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;
|
|
71
86
|
allProgrammingLanguageManifest(): Promise<Array<ProgrammingLanguageManifest>>;
|
|
72
87
|
pushProgrammingLanguageManifest(programmingLanguageManifest: ProgrammingLanguageManifest): Promise<ProgrammingLanguageManifest>;
|
|
73
88
|
allServiceAccounts(): Promise<Array<ServiceAccount>>;
|
|
@@ -4,6 +4,10 @@ const graphql_request_1 = require("graphql-request");
|
|
|
4
4
|
const types_1 = require("../core/types");
|
|
5
5
|
const locale_merger_1 = require("./locale-merger");
|
|
6
6
|
const _ = require("lodash");
|
|
7
|
+
// Manifest fields an fcode-api older than this CLI may not know. The manifest query falls back to a
|
|
8
|
+
// narrower selection when the error names one of them, so a CLI stays usable against an
|
|
9
|
+
// on-premises api that has not been updated yet.
|
|
10
|
+
const UNKNOWN_MANIFEST_FIELDS = ['isNative', 'inheritedDependencies'];
|
|
7
11
|
// Shared team version selections so every query/mutation returns the same shape.
|
|
8
12
|
const TEAM_VERSION_FIELDS = `
|
|
9
13
|
id
|
|
@@ -67,6 +71,13 @@ const PROCESS_FIELDS = `
|
|
|
67
71
|
appRole
|
|
68
72
|
authMode
|
|
69
73
|
}
|
|
74
|
+
uiTrigger {
|
|
75
|
+
enabled
|
|
76
|
+
locationId
|
|
77
|
+
label
|
|
78
|
+
icon
|
|
79
|
+
awaitResult
|
|
80
|
+
}
|
|
70
81
|
dependencies {
|
|
71
82
|
scopedToProcess
|
|
72
83
|
}
|
|
@@ -351,9 +362,9 @@ class FcodeGraphQLClient {
|
|
|
351
362
|
return Promise.reject(this.cleanError(error));
|
|
352
363
|
}
|
|
353
364
|
// The cloud team's parent order is the inheritance precedence the merge must replicate; the
|
|
354
|
-
// local
|
|
355
|
-
const {
|
|
356
|
-
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));
|
|
357
368
|
(0, locale_merger_1.assertLocaleFilesCanCoexist)(merged);
|
|
358
369
|
return merged;
|
|
359
370
|
}
|
|
@@ -429,6 +440,21 @@ class FcodeGraphQLClient {
|
|
|
429
440
|
settings.formSettings.authMode = process.form.authMode;
|
|
430
441
|
}
|
|
431
442
|
}
|
|
443
|
+
if (process.uiTrigger) {
|
|
444
|
+
settings.uiTrigger = { enabled: process.uiTrigger.enabled };
|
|
445
|
+
if (process.uiTrigger.locationId) {
|
|
446
|
+
settings.uiTrigger.locationId = process.uiTrigger.locationId;
|
|
447
|
+
}
|
|
448
|
+
if (process.uiTrigger.label) {
|
|
449
|
+
settings.uiTrigger.label = process.uiTrigger.label;
|
|
450
|
+
}
|
|
451
|
+
if (process.uiTrigger.icon) {
|
|
452
|
+
settings.uiTrigger.icon = process.uiTrigger.icon;
|
|
453
|
+
}
|
|
454
|
+
if (process.uiTrigger.awaitResult !== undefined) {
|
|
455
|
+
settings.uiTrigger.awaitResult = process.uiTrigger.awaitResult;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
432
458
|
const input = {
|
|
433
459
|
name: process.name,
|
|
434
460
|
parametersSchema: process.script.parametersSchema,
|
|
@@ -645,6 +671,13 @@ class FcodeGraphQLClient {
|
|
|
645
671
|
isNative
|
|
646
672
|
gitUrl
|
|
647
673
|
}
|
|
674
|
+
inheritedDependencies {
|
|
675
|
+
name
|
|
676
|
+
semVer
|
|
677
|
+
isNative
|
|
678
|
+
gitUrl
|
|
679
|
+
resolvingTeamSlug
|
|
680
|
+
}
|
|
648
681
|
}
|
|
649
682
|
}
|
|
650
683
|
`, { programmingLanguage })
|
|
@@ -654,7 +687,10 @@ class FcodeGraphQLClient {
|
|
|
654
687
|
.catch((error) => {
|
|
655
688
|
// TODO: This is a workaround backward compatibility with old versions of fcode-api.
|
|
656
689
|
// Remove when full on-premises installations are updated.
|
|
657
|
-
|
|
690
|
+
// `inheritedDependencies` is listed for the same reason `isNative` is: an api predating
|
|
691
|
+
// dependency inheritance rejects the whole query over the unknown field, and the fallback
|
|
692
|
+
// below simply reads no inherited dependencies.
|
|
693
|
+
if (error.response?.errors?.[0]?.message && UNKNOWN_MANIFEST_FIELDS.some((field) => error.response.errors[0].message.includes(field))) {
|
|
658
694
|
return this.client
|
|
659
695
|
.request((0, graphql_request_1.gql) `
|
|
660
696
|
query getProgrammingLanguageManifest($programmingLanguage: ProgrammingLanguage!) {
|
|
@@ -732,10 +768,25 @@ class FcodeGraphQLClient {
|
|
|
732
768
|
metadata.form.authMode = settings.formSettings.authMode;
|
|
733
769
|
}
|
|
734
770
|
}
|
|
771
|
+
if (settings?.uiTrigger) {
|
|
772
|
+
metadata.uiTrigger = { enabled: Boolean(settings.uiTrigger.enabled) };
|
|
773
|
+
if (settings.uiTrigger.locationId) {
|
|
774
|
+
metadata.uiTrigger.locationId = settings.uiTrigger.locationId;
|
|
775
|
+
}
|
|
776
|
+
if (settings.uiTrigger.label) {
|
|
777
|
+
metadata.uiTrigger.label = settings.uiTrigger.label;
|
|
778
|
+
}
|
|
779
|
+
if (settings.uiTrigger.icon) {
|
|
780
|
+
metadata.uiTrigger.icon = settings.uiTrigger.icon;
|
|
781
|
+
}
|
|
782
|
+
if (settings.uiTrigger.awaitResult) {
|
|
783
|
+
metadata.uiTrigger.awaitResult = true;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
735
786
|
return new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug));
|
|
736
787
|
}
|
|
737
788
|
/**
|
|
738
|
-
* GraphQL nests the webhook configuration under `settings`, while
|
|
789
|
+
* GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
|
|
739
790
|
* keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
|
|
740
791
|
* undefined.
|
|
741
792
|
*/
|
|
@@ -744,7 +795,7 @@ class FcodeGraphQLClient {
|
|
|
744
795
|
slug: team.slug,
|
|
745
796
|
zoneId: team.zoneId,
|
|
746
797
|
errorHandlerConfig: team.errorHandlerConfig,
|
|
747
|
-
|
|
798
|
+
parentTeams: team.parentTeams ?? [],
|
|
748
799
|
};
|
|
749
800
|
const webhookAuth = team.settings?.webhookAuth;
|
|
750
801
|
if (webhookAuth?.variableKey) {
|
|
@@ -896,7 +947,10 @@ class FcodeGraphQLClient {
|
|
|
896
947
|
processId
|
|
897
948
|
tag
|
|
898
949
|
}
|
|
899
|
-
|
|
950
|
+
parentTeams {
|
|
951
|
+
slug
|
|
952
|
+
version
|
|
953
|
+
}
|
|
900
954
|
settings {
|
|
901
955
|
webhookAuth {
|
|
902
956
|
headerName
|
|
@@ -923,7 +977,10 @@ class FcodeGraphQLClient {
|
|
|
923
977
|
processId
|
|
924
978
|
tag
|
|
925
979
|
}
|
|
926
|
-
|
|
980
|
+
parentTeams {
|
|
981
|
+
slug
|
|
982
|
+
version
|
|
983
|
+
}
|
|
927
984
|
settings {
|
|
928
985
|
webhookAuth {
|
|
929
986
|
headerName
|
|
@@ -941,15 +998,66 @@ class FcodeGraphQLClient {
|
|
|
941
998
|
}
|
|
942
999
|
mapProgrammingLanguageManifest(programmingLanguageManifest) {
|
|
943
1000
|
const allDependencies = programmingLanguageManifest.nextInstallation?.dependencies || programmingLanguageManifest.dependencies;
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1001
|
+
return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.programmingLanguage, programmingLanguageManifest.programmingLanguage, this.mapDependencies(allDependencies),
|
|
1002
|
+
// Absent when the api predates dependency inheritance, or when the workspace has no parents.
|
|
1003
|
+
this.mapDependencies(programmingLanguageManifest.inheritedDependencies));
|
|
1004
|
+
}
|
|
1005
|
+
mapDependencies(dependencies) {
|
|
1006
|
+
return _.map(dependencies, (dependency) => {
|
|
1007
|
+
if (dependency.isNative) {
|
|
1008
|
+
return null;
|
|
947
1009
|
}
|
|
948
|
-
return
|
|
1010
|
+
return {
|
|
1011
|
+
name: dependency.name,
|
|
1012
|
+
specifier: dependency.gitUrl ?? dependency.semVer,
|
|
1013
|
+
...(dependency.resolvingTeamSlug && { resolvingTeamSlug: dependency.resolvingTeamSlug }),
|
|
1014
|
+
};
|
|
949
1015
|
})
|
|
950
1016
|
.filter(Boolean)
|
|
951
|
-
.sort((a, b) => a
|
|
952
|
-
|
|
1017
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
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');
|
|
953
1061
|
}
|
|
954
1062
|
allProgrammingLanguageManifest() {
|
|
955
1063
|
const result = [];
|
|
@@ -983,7 +1091,11 @@ class FcodeGraphQLClient {
|
|
|
983
1091
|
}
|
|
984
1092
|
`, {
|
|
985
1093
|
id: programmingLanguageManifest.id,
|
|
986
|
-
input: {
|
|
1094
|
+
input: {
|
|
1095
|
+
// Only name and specifier: DependencyUpdateInput has no other fields, and a workspace
|
|
1096
|
+
// pushes its own list — inherited entries live in a separate file and never get here.
|
|
1097
|
+
dependencies: programmingLanguageManifest.dependencies.map(({ name, specifier }) => ({ name, specifier })),
|
|
1098
|
+
},
|
|
987
1099
|
})
|
|
988
1100
|
.then(({ updateProgrammingLanguageManifest: programmingLanguageManifest }) => {
|
|
989
1101
|
return this.client
|
|
@@ -8,11 +8,27 @@
|
|
|
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
|
|
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
|
|
14
29
|
*/
|
|
15
30
|
export declare function resolveLocalTranslations(requestedLocale?: string, workspaceDir?: string): {
|
|
16
31
|
locale?: string;
|
|
17
32
|
translations: Record<string, string>;
|
|
33
|
+
byLocale: Record<string, Record<string, string>>;
|
|
18
34
|
};
|