@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.
Files changed (123) hide show
  1. package/README.md +334 -117
  2. package/dist/commands/clone.d.ts +0 -12
  3. package/dist/commands/clone.js +22 -135
  4. package/dist/commands/{settings → config}/get.d.ts +1 -1
  5. package/dist/commands/{settings → config}/get.js +7 -7
  6. package/dist/commands/{settings → config}/remove.d.ts +1 -1
  7. package/dist/commands/{settings → config}/remove.js +6 -6
  8. package/dist/commands/{settings → config}/set.d.ts +1 -1
  9. package/dist/commands/{settings → config}/set.js +8 -8
  10. package/dist/commands/dependencies/diff.d.ts +9 -0
  11. package/dist/commands/dependencies/diff.js +15 -0
  12. package/dist/commands/dependencies/pull.d.ts +9 -0
  13. package/dist/commands/dependencies/pull.js +44 -1
  14. package/dist/commands/dependencies/status.d.ts +11 -0
  15. package/dist/commands/dependencies/status.js +43 -0
  16. package/dist/commands/diff.d.ts +12 -0
  17. package/dist/commands/diff.js +48 -0
  18. package/dist/commands/http.d.ts +1 -1
  19. package/dist/commands/http.js +7 -7
  20. package/dist/commands/i18n/diff.d.ts +9 -0
  21. package/dist/commands/i18n/diff.js +15 -0
  22. package/dist/commands/i18n/status.d.ts +3 -0
  23. package/dist/commands/i18n/status.js +1 -0
  24. package/dist/commands/modules/diff.d.ts +9 -0
  25. package/dist/commands/modules/diff.js +15 -0
  26. package/dist/commands/modules/status.d.ts +3 -0
  27. package/dist/commands/modules/status.js +1 -0
  28. package/dist/commands/processes/diff.d.ts +9 -0
  29. package/dist/commands/processes/diff.js +15 -0
  30. package/dist/commands/processes/status.d.ts +3 -0
  31. package/dist/commands/processes/status.js +1 -0
  32. package/dist/commands/pull.js +3 -3
  33. package/dist/commands/push.js +2 -2
  34. package/dist/commands/{team → settings}/aliases/delete.d.ts +1 -1
  35. package/dist/commands/{team → settings}/aliases/delete.js +6 -6
  36. package/dist/commands/{team → settings}/aliases/list.d.ts +1 -1
  37. package/dist/commands/{team → settings}/aliases/list.js +8 -8
  38. package/dist/commands/{team → settings}/aliases/set.d.ts +1 -1
  39. package/dist/commands/{team → settings}/aliases/set.js +8 -8
  40. package/dist/commands/settings/diff.d.ts +8 -0
  41. package/dist/commands/settings/diff.js +39 -0
  42. package/dist/commands/{team/push.d.ts → settings/pull.d.ts} +3 -3
  43. package/dist/commands/settings/pull.js +41 -0
  44. package/dist/commands/settings/push.d.ts +13 -0
  45. package/dist/commands/settings/push.js +103 -0
  46. package/dist/commands/settings/status.d.ts +8 -0
  47. package/dist/commands/settings/status.js +46 -0
  48. package/dist/commands/{team → settings}/versions/create.d.ts +1 -2
  49. package/dist/commands/settings/versions/create.js +36 -0
  50. package/dist/commands/{team → settings}/versions/delete.d.ts +1 -1
  51. package/dist/commands/{team → settings}/versions/delete.js +10 -10
  52. package/dist/commands/{team → settings}/versions/list.d.ts +1 -1
  53. package/dist/commands/{team → settings}/versions/list.js +8 -8
  54. package/dist/commands/status.d.ts +3 -0
  55. package/dist/commands/status.js +10 -7
  56. package/dist/commands/team/clone.d.ts +14 -0
  57. package/dist/commands/team/clone.js +91 -0
  58. package/dist/commands/team/pull.d.ts +3 -4
  59. package/dist/commands/team/pull.js +25 -19
  60. package/dist/commands/team/status.d.ts +3 -4
  61. package/dist/commands/team/status.js +34 -27
  62. package/dist/commands/variables/diff.d.ts +9 -0
  63. package/dist/commands/variables/diff.js +15 -0
  64. package/dist/commands/variables/status.d.ts +3 -0
  65. package/dist/commands/variables/status.js +1 -0
  66. package/dist/config/dev-team.d.ts +48 -0
  67. package/dist/config/dev-team.js +62 -0
  68. package/dist/config/workspace.d.ts +2 -2
  69. package/dist/config/workspace.js +4 -2
  70. package/dist/core/commands/base-command.js +5 -0
  71. package/dist/core/commands/dependency-command.d.ts +2 -1
  72. package/dist/core/commands/dependency-command.js +3 -0
  73. package/dist/core/commands/i18n-command.js +2 -0
  74. package/dist/core/commands/modules-command.js +2 -0
  75. package/dist/core/commands/processes-command.js +2 -0
  76. package/dist/core/commands/resource-command.d.ts +26 -1
  77. package/dist/core/commands/resource-command.js +100 -3
  78. package/dist/core/commands/variables-command.d.ts +7 -0
  79. package/dist/core/commands/variables-command.js +13 -0
  80. package/dist/core/dependencies/env-dependencies-command.js +6 -3
  81. package/dist/core/executors/javascript-execution-task.js +36 -5
  82. package/dist/core/types.d.ts +59 -9
  83. package/dist/core/types.js +64 -10
  84. package/dist/core/utils/dependencies.js +4 -1
  85. package/dist/env.js +1 -1
  86. package/dist/repositories/process.d.ts +8 -2
  87. package/dist/repositories/process.js +29 -15
  88. package/dist/repositories/programming-language-manifest.d.ts +31 -1
  89. package/dist/repositories/programming-language-manifest.js +96 -2
  90. package/dist/repositories/settings.d.ts +52 -0
  91. package/dist/repositories/settings.js +214 -0
  92. package/dist/runtime/libs/python/fcode/context.py +42 -8
  93. package/dist/services/dashboard-client.d.ts +31 -0
  94. package/dist/services/dashboard-client.js +99 -0
  95. package/dist/services/dev-team-service.d.ts +41 -0
  96. package/dist/services/dev-team-service.js +162 -0
  97. package/dist/services/fcode-graphql-client.d.ts +17 -2
  98. package/dist/services/fcode-graphql-client.js +127 -15
  99. package/dist/services/locale-resolver.d.ts +17 -1
  100. package/dist/services/locale-resolver.js +62 -7
  101. package/dist/services/resource-differ.d.ts +60 -0
  102. package/dist/services/resource-differ.js +135 -0
  103. package/dist/services/skills-installer.d.ts +16 -0
  104. package/dist/services/skills-installer.js +77 -0
  105. package/dist/services/team-versioning.d.ts +16 -0
  106. package/dist/services/team-versioning.js +40 -0
  107. package/dist/services/webhook-auth.js +1 -1
  108. package/dist/services/workspace-cloner.d.ts +31 -0
  109. package/dist/services/workspace-cloner.js +95 -0
  110. package/dist/utils/dev-slug.d.ts +12 -0
  111. package/dist/utils/dev-slug.js +37 -0
  112. package/dist/utils/diff.d.ts +19 -0
  113. package/dist/utils/diff.js +58 -0
  114. package/dist/utils/gitignore.d.ts +3 -1
  115. package/dist/utils/gitignore.js +11 -2
  116. package/dist/utils/slugify.d.ts +17 -0
  117. package/dist/utils/slugify.js +41 -0
  118. package/oclif.manifest.json +433 -113
  119. package/package.json +12 -7
  120. package/dist/commands/team/push.js +0 -52
  121. package/dist/commands/team/versions/create.js +0 -42
  122. package/dist/repositories/team.d.ts +0 -31
  123. 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 team.json and the REST API
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 team.json may lag behind it, so the cloud is asked, not the file.
355
- const { parentTeamSlugs } = await this.team();
356
- const merged = (0, locale_merger_1.mergeInheritedLocales)(locales, parentTeamSlugs ?? []);
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
- if (error.response && error.response.errors && error.response.errors[0] && error.response.errors[0].message.includes('isNative')) {
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 team.json and the REST API
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
- parentTeamSlugs: team.parentTeamSlugs ?? [],
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
- parentTeamSlugs
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
- parentTeamSlugs
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
- const dependencies = _.map(allDependencies, (dependency) => {
945
- if (!dependency.isNative) {
946
- return { name: dependency.name, specifier: dependency.gitUrl ?? dependency.semVer };
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 null;
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 && b && a.name.localeCompare(b.name));
952
- return new types_1.ProgrammingLanguageManifest(programmingLanguageManifest.id, programmingLanguageManifest.programmingLanguage, programmingLanguageManifest.programmingLanguage, dependencies);
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: { dependencies: programmingLanguageManifest.dependencies },
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 underneath
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
  };