@factorialco/fcode-cli 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/dist/commands/app/push.d.ts +13 -0
- package/dist/commands/app/push.js +147 -0
- package/dist/commands/i18n/status.js +9 -0
- package/dist/commands/login.js +2 -1
- package/dist/commands/modules/pull.js +5 -4
- package/dist/commands/modules/push.js +1 -1
- package/dist/commands/modules/status.js +9 -0
- package/dist/commands/processes/pull.js +5 -4
- package/dist/commands/processes/push.js +1 -1
- package/dist/commands/processes/status.js +9 -0
- package/dist/commands/runtime-env.js +2 -2
- package/dist/commands/team/clone.js +1 -1
- package/dist/commands/team/pull.js +3 -2
- package/dist/commands/team/status.d.ts +2 -0
- package/dist/commands/team/status.js +44 -2
- package/dist/config/dev-team.d.ts +15 -0
- package/dist/config/dev-team.js +54 -1
- package/dist/config/settings.d.ts +1 -0
- package/dist/config/settings.js +6 -0
- package/dist/core/commands/resource-command.d.ts +4 -1
- package/dist/core/commands/resource-command.js +9 -2
- package/dist/core/dependencies/javascript-env-dependencies.js +4 -4
- package/dist/core/dependencies/python-env-dependencies.js +5 -4
- package/dist/core/executors/execution-task.js +1 -1
- package/dist/core/executors/javascript-execution-task.js +21 -17
- package/dist/core/executors/python-execution-task.js +11 -10
- package/dist/core/resource-pusher.js +13 -7
- package/dist/core/types.d.ts +16 -3
- package/dist/core/types.js +31 -6
- package/dist/hooks/init/auth.d.ts +19 -0
- package/dist/hooks/init/auth.js +40 -6
- package/dist/repositories/locale.js +5 -3
- package/dist/repositories/process.js +4 -2
- package/dist/repositories/script-library.js +3 -2
- package/dist/runtime/libs/python/fcode/datastore.py +3 -1
- package/dist/services/app-metadata-files.d.ts +98 -0
- package/dist/services/app-metadata-files.js +363 -0
- package/dist/services/dashboard-client.d.ts +59 -0
- package/dist/services/dashboard-client.js +116 -10
- package/dist/services/dev-team-service.d.ts +4 -1
- package/dist/services/dev-team-service.js +31 -17
- package/dist/services/fcode-graphql-client.d.ts +26 -7
- package/dist/services/fcode-graphql-client.js +90 -41
- package/dist/services/skills-installer.js +2 -2
- package/dist/services/version-pull-reporter.d.ts +20 -0
- package/dist/services/version-pull-reporter.js +33 -0
- package/dist/utils/child-process.d.ts +26 -0
- package/dist/utils/child-process.js +60 -0
- package/oclif.manifest.json +35 -2
- package/package.json +4 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DashboardApiError = void 0;
|
|
3
4
|
exports.dashboardApiUrl = dashboardApiUrl;
|
|
4
5
|
const node_fetch_1 = require("node-fetch");
|
|
5
6
|
const auth_service_1 = require("./auth-service");
|
|
@@ -12,6 +13,45 @@ const global_1 = require("../config/global");
|
|
|
12
13
|
*/
|
|
13
14
|
/** Max the dashboard's list endpoints accept (`listQuerySchema`), so a team is one round trip. */
|
|
14
15
|
const PAGE_SIZE = 200;
|
|
16
|
+
/** A non-2xx answer from the dashboard, carrying the RFC 9457 problem details it sent. */
|
|
17
|
+
class DashboardApiError extends Error {
|
|
18
|
+
constructor(status, path, detail, code) {
|
|
19
|
+
super(`Dashboard API ${status} on ${path}: ${detail}`);
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.path = path;
|
|
22
|
+
this.detail = detail;
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.name = 'DashboardApiError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.DashboardApiError = DashboardApiError;
|
|
28
|
+
const appReadmePath = (id) => `/apps/${id}/readme`;
|
|
29
|
+
function mapApp({ id, name, description, publication_state, logo_content_type }) {
|
|
30
|
+
return { id, name, description, publicationState: publication_state, logoContentType: logo_content_type ?? null };
|
|
31
|
+
}
|
|
32
|
+
function mapMarketplaceMetadata(raw) {
|
|
33
|
+
return {
|
|
34
|
+
fullDescription: raw.full_description,
|
|
35
|
+
tagline: raw.tagline,
|
|
36
|
+
datoCategoryId: raw.dato_category_id,
|
|
37
|
+
supportLink: raw.support_link,
|
|
38
|
+
helpLink: raw.help_link,
|
|
39
|
+
datocmsId: raw.datocms_id,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function mapRelease(raw) {
|
|
43
|
+
return {
|
|
44
|
+
id: raw.id,
|
|
45
|
+
version: raw.version,
|
|
46
|
+
comments: raw.comments,
|
|
47
|
+
status: raw.status,
|
|
48
|
+
error: raw.error,
|
|
49
|
+
requestedBy: raw.requested_by,
|
|
50
|
+
deployedAt: raw.deployed_at,
|
|
51
|
+
stableAt: raw.stable_at,
|
|
52
|
+
createdAt: raw.created_at,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
15
55
|
/**
|
|
16
56
|
* `https://code.factorialhr.com/platform` → `https://code.factorialhr.com/dashboard/api/v1`.
|
|
17
57
|
* The dashboard is served under `/dashboard` on the same origin as the platform; environments
|
|
@@ -43,7 +83,43 @@ class DashboardClient {
|
|
|
43
83
|
sort_by: 'name',
|
|
44
84
|
sort_dir: 'asc',
|
|
45
85
|
});
|
|
46
|
-
return items.map(
|
|
86
|
+
return items.map(mapApp);
|
|
87
|
+
}
|
|
88
|
+
async getApp(appId) {
|
|
89
|
+
return mapApp(await this.request('GET', `/apps/${appId}`));
|
|
90
|
+
}
|
|
91
|
+
async getAppReadme(appId) {
|
|
92
|
+
const { readme } = await this.request('GET', appReadmePath(appId));
|
|
93
|
+
return { readme: readme ?? null };
|
|
94
|
+
}
|
|
95
|
+
async putAppReadme(appId, readme) {
|
|
96
|
+
const response = await this.request('PUT', appReadmePath(appId), { body: { readme } });
|
|
97
|
+
return { readme: response.readme ?? null };
|
|
98
|
+
}
|
|
99
|
+
async getAppMarketplace(appId) {
|
|
100
|
+
const { metadata } = await this.request('GET', `/apps/${appId}/marketplace`);
|
|
101
|
+
return { metadata: metadata ? mapMarketplaceMetadata(metadata) : null };
|
|
102
|
+
}
|
|
103
|
+
/** Full replace: every field travels, nulls included, since an absent key clears on the dashboard. */
|
|
104
|
+
async putAppMarketplace(appId, input) {
|
|
105
|
+
const raw = await this.request('PUT', `/apps/${appId}/marketplace`, {
|
|
106
|
+
body: {
|
|
107
|
+
full_description: input.fullDescription,
|
|
108
|
+
tagline: input.tagline,
|
|
109
|
+
dato_category_id: input.datoCategoryId,
|
|
110
|
+
support_link: input.supportLink,
|
|
111
|
+
help_link: input.helpLink,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return mapMarketplaceMetadata(raw);
|
|
115
|
+
}
|
|
116
|
+
async listAppReleases(appId) {
|
|
117
|
+
const items = await this.getAllPages(`/apps/${appId}/releases`);
|
|
118
|
+
return items.map(mapRelease);
|
|
119
|
+
}
|
|
120
|
+
/** The App's logo bytes, or null when it has none. */
|
|
121
|
+
async getAppLogo(appId) {
|
|
122
|
+
return this.getBinary(`/apps/${appId}/logo`);
|
|
47
123
|
}
|
|
48
124
|
/** The global workspaces owned by a development team. */
|
|
49
125
|
async listTeamGlobalWorkspaces(teamId) {
|
|
@@ -76,32 +152,62 @@ class DashboardClient {
|
|
|
76
152
|
return items;
|
|
77
153
|
}
|
|
78
154
|
async get(path, query) {
|
|
79
|
-
|
|
80
|
-
|
|
155
|
+
return this.request('GET', path, { query });
|
|
156
|
+
}
|
|
157
|
+
async request(method, path, options = {}) {
|
|
158
|
+
const response = await this.send(method, path, options);
|
|
159
|
+
return (await response.json());
|
|
160
|
+
}
|
|
161
|
+
async getBinary(path) {
|
|
162
|
+
try {
|
|
163
|
+
const response = await this.send('GET', path, { accept: 'image/*' });
|
|
164
|
+
return { buffer: await response.buffer(), contentType: response.headers.get('content-type') ?? '' };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof DashboardApiError && error.status === 404) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async send(method, path, { query, body, accept }) {
|
|
174
|
+
const search = query ? `?${new URLSearchParams(query).toString()}` : '';
|
|
175
|
+
const url = `${this.baseUrl}${path}${search}`;
|
|
176
|
+
const init = async () => ({
|
|
177
|
+
method,
|
|
178
|
+
headers: await this.headers(accept, body !== undefined),
|
|
179
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
180
|
+
});
|
|
181
|
+
let response = await (0, node_fetch_1.default)(url, await init());
|
|
81
182
|
// The token's group membership is snapshotted at issuance (FCT-61231), so a team joined after
|
|
82
183
|
// login reads as a denial. Re-mint once and retry before believing it.
|
|
83
184
|
if (response.status === 401 || response.status === 403) {
|
|
84
185
|
if (await auth_service_1.default.refreshSession(this.cloudUrl)) {
|
|
85
|
-
response = await (0, node_fetch_1.default)(url,
|
|
186
|
+
response = await (0, node_fetch_1.default)(url, await init());
|
|
86
187
|
}
|
|
87
188
|
}
|
|
88
189
|
if (!response.ok) {
|
|
89
|
-
|
|
190
|
+
const { detail, code } = await this.errorDetail(response);
|
|
191
|
+
throw new DashboardApiError(response.status, path, detail, code);
|
|
90
192
|
}
|
|
91
|
-
return
|
|
193
|
+
return response;
|
|
92
194
|
}
|
|
93
|
-
async headers() {
|
|
195
|
+
async headers(accept = 'application/json', withBody = false) {
|
|
94
196
|
const { token } = await this.globalConfig.getAuthInfo(this.cloudUrl);
|
|
95
|
-
return {
|
|
197
|
+
return {
|
|
198
|
+
accept,
|
|
199
|
+
authorization: `Bearer ${token.access_token}`,
|
|
200
|
+
...(withBody ? { 'content-type': 'application/json' } : {}),
|
|
201
|
+
};
|
|
96
202
|
}
|
|
97
203
|
async errorDetail(response) {
|
|
98
204
|
try {
|
|
99
205
|
const body = await response.text();
|
|
100
206
|
const parsed = JSON.parse(body);
|
|
101
|
-
return parsed.detail ?? parsed.message ?? body;
|
|
207
|
+
return { detail: parsed.detail ?? parsed.message ?? body, code: parsed.code };
|
|
102
208
|
}
|
|
103
209
|
catch {
|
|
104
|
-
return 'no response body';
|
|
210
|
+
return { detail: 'no response body' };
|
|
105
211
|
}
|
|
106
212
|
}
|
|
107
213
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import DashboardClient, { DevelopmentTeam, DevelopmentTeamApp, DevelopmentTeamGlobalWorkspace } from './dashboard-client';
|
|
2
2
|
import { DevTeamMetadata } from '../config/dev-team';
|
|
3
|
+
import { AppMetadataSource } from './app-metadata-files';
|
|
3
4
|
/** What the dashboard currently says the team holds. */
|
|
4
5
|
export interface DevTeamRemote {
|
|
5
6
|
apps: DevelopmentTeamApp[];
|
|
@@ -33,6 +34,8 @@ export interface TeamSyncOutcome {
|
|
|
33
34
|
folder: string;
|
|
34
35
|
action: 'cloned' | 'pulled';
|
|
35
36
|
error?: Error;
|
|
37
|
+
/** Metadata files kept or not refreshed; reported, never a failure. */
|
|
38
|
+
warnings?: string[];
|
|
36
39
|
}
|
|
37
40
|
export interface TeamSyncResult {
|
|
38
41
|
outcomes: TeamSyncOutcome[];
|
|
@@ -45,7 +48,7 @@ export interface TeamSyncResult {
|
|
|
45
48
|
* that lands. A failing workspace is reported and the run carries on: with a dozen Apps, one bad
|
|
46
49
|
* workspace must not cost the other eleven.
|
|
47
50
|
*/
|
|
48
|
-
export declare function syncTeam(rootDir: string, metadata: DevTeamMetadata, remote: DevTeamRemote): Promise<TeamSyncResult>;
|
|
51
|
+
export declare function syncTeam(rootDir: string, metadata: DevTeamMetadata, remote: DevTeamRemote, source: AppMetadataSource): Promise<TeamSyncResult>;
|
|
49
52
|
/** Prints the per-workspace result and reports whether anything failed. */
|
|
50
53
|
export declare function reportOutcomes({ outcomes, removedFolders }: TeamSyncResult): boolean;
|
|
51
54
|
/**
|
|
@@ -11,6 +11,7 @@ const path = require("node:path");
|
|
|
11
11
|
const color_1 = require("@oclif/color");
|
|
12
12
|
const pull_1 = require("../commands/pull");
|
|
13
13
|
const dev_team_1 = require("../config/dev-team");
|
|
14
|
+
const app_metadata_files_1 = require("./app-metadata-files");
|
|
14
15
|
const skills_installer_1 = require("./skills-installer");
|
|
15
16
|
const dev_slug_1 = require("../utils/dev-slug");
|
|
16
17
|
const workspace_cloner_1 = require("./workspace-cloner");
|
|
@@ -61,34 +62,39 @@ function writeSkillsGitIgnore(dir) {
|
|
|
61
62
|
fs.outputFileSync(gitIgnorePath, ['# Agent skills are installed once at the development-team root and linked here.', '**/.claude/skills', ''].join('\n'));
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* would try to create it again and stop at "already cloned".
|
|
65
|
+
* Metadata warnings go into the caller's sink rather than the return value: the workspace pull
|
|
66
|
+
* that follows may throw, and what was said about the files before that still holds.
|
|
67
67
|
*/
|
|
68
|
-
async function cloneApp(
|
|
68
|
+
async function cloneApp(context, app, folder, warnings) {
|
|
69
|
+
const { rootDir, metadata, source } = context;
|
|
69
70
|
const slug = (0, dev_slug_1.devSlug)(app.id);
|
|
70
71
|
const appDir = path.join(rootDir, folder);
|
|
71
72
|
const workspaceDir = path.join(appDir, dev_team_1.APP_WORKSPACE_DIR);
|
|
72
73
|
cli_1.default.log(color_1.default.cyan(`\n📦 ${app.name} → ${folder}/${dev_team_1.APP_WORKSPACE_DIR} (${slug})`));
|
|
73
74
|
await (0, workspace_cloner_1.scaffoldWorkspace)(workspaceDir, { url: metadata.cloudUrl, slug });
|
|
74
|
-
|
|
75
|
+
const recorded = { name: app.name, description: app.description, folder, devWorkspaceSlug: slug };
|
|
76
|
+
metadata.apps[app.id] = recorded;
|
|
75
77
|
(0, dev_team_1.writeAppSettings)(appDir, { id: app.id, name: app.name, description: app.description });
|
|
78
|
+
warnings.push(...(await (0, app_metadata_files_1.syncAppMetadata)(source, appDir, app, recorded)).warnings);
|
|
76
79
|
writeSkillsGitIgnore(appDir);
|
|
77
80
|
linkSkills(rootDir, appDir, workspaceDir);
|
|
78
81
|
await (0, workspace_cloner_1.runInWorkspace)(workspaceDir, () => pull_1.default.run([]));
|
|
79
82
|
}
|
|
80
|
-
async function refreshApp(
|
|
83
|
+
async function refreshApp(context, app, recorded, warnings) {
|
|
84
|
+
const { rootDir, metadata, source } = context;
|
|
81
85
|
const appDir = path.join(rootDir, recorded.folder);
|
|
82
86
|
const workspaceDir = path.join(appDir, dev_team_1.APP_WORKSPACE_DIR);
|
|
83
87
|
cli_1.default.log(color_1.default.cyan(`\n📦 ${app.name} → ${recorded.folder}/${dev_team_1.APP_WORKSPACE_DIR}`));
|
|
84
88
|
// A recorded App whose workspace is gone — a failed first clone, or a directory the user
|
|
85
89
|
// removed — is laid out again rather than failing every pull from here on.
|
|
86
90
|
if (!fs.pathExistsSync(path.join(workspaceDir, '.fcode'))) {
|
|
87
|
-
await (0, workspace_cloner_1.scaffoldWorkspace)(workspaceDir, { url: cloudUrl, slug: recorded.devWorkspaceSlug });
|
|
91
|
+
await (0, workspace_cloner_1.scaffoldWorkspace)(workspaceDir, { url: metadata.cloudUrl, slug: recorded.devWorkspaceSlug });
|
|
88
92
|
}
|
|
89
93
|
// Name and description live in the dashboard; the local copy is a mirror refreshed on every
|
|
90
|
-
// pull. There is deliberately no team:push —
|
|
94
|
+
// pull. There is deliberately no team:push — code is pushed inside a workspace, and the App's
|
|
95
|
+
// README.md / MARKETPLACE.md with `fcode app:push`.
|
|
91
96
|
(0, dev_team_1.writeAppSettings)(appDir, { id: app.id, name: app.name, description: app.description });
|
|
97
|
+
warnings.push(...(await (0, app_metadata_files_1.syncAppMetadata)(source, appDir, app, recorded)).warnings);
|
|
92
98
|
writeSkillsGitIgnore(appDir);
|
|
93
99
|
recorded.name = app.name;
|
|
94
100
|
recorded.description = app.description;
|
|
@@ -113,27 +119,32 @@ async function syncGlobalWorkspace(rootDir, workspace, metadata) {
|
|
|
113
119
|
(0, skills_installer_1.linkSkillsInto)(workspaceDir, rootDir);
|
|
114
120
|
await (0, workspace_cloner_1.runInWorkspace)(workspaceDir, () => pull_1.default.run([]));
|
|
115
121
|
}
|
|
116
|
-
async function syncApps(rootDir, metadata, plan) {
|
|
122
|
+
async function syncApps(rootDir, metadata, plan, source) {
|
|
117
123
|
const outcomes = [];
|
|
124
|
+
const context = { rootDir, metadata, source };
|
|
118
125
|
for (const app of plan.toClone) {
|
|
119
126
|
const folder = assignFolder(app, metadata);
|
|
127
|
+
const warnings = [];
|
|
128
|
+
const outcome = { name: app.name, folder, action: 'cloned', warnings };
|
|
120
129
|
try {
|
|
121
|
-
await cloneApp(
|
|
122
|
-
outcomes.push({ name: app.name, folder, action: 'cloned' });
|
|
130
|
+
await cloneApp(context, app, folder, warnings);
|
|
123
131
|
}
|
|
124
132
|
catch (error) {
|
|
125
|
-
|
|
133
|
+
outcome.error = error;
|
|
126
134
|
}
|
|
135
|
+
outcomes.push(outcome);
|
|
127
136
|
}
|
|
128
137
|
for (const app of plan.toRefresh) {
|
|
129
138
|
const recorded = metadata.apps[app.id];
|
|
139
|
+
const warnings = [];
|
|
140
|
+
const outcome = { name: app.name, folder: recorded.folder, action: 'pulled', warnings };
|
|
130
141
|
try {
|
|
131
|
-
await refreshApp(
|
|
132
|
-
outcomes.push({ name: app.name, folder: recorded.folder, action: 'pulled' });
|
|
142
|
+
await refreshApp(context, app, recorded, warnings);
|
|
133
143
|
}
|
|
134
144
|
catch (error) {
|
|
135
|
-
|
|
145
|
+
outcome.error = error;
|
|
136
146
|
}
|
|
147
|
+
outcomes.push(outcome);
|
|
137
148
|
}
|
|
138
149
|
return outcomes;
|
|
139
150
|
}
|
|
@@ -162,12 +173,12 @@ async function syncGlobalWorkspaces(rootDir, metadata, plan) {
|
|
|
162
173
|
* that lands. A failing workspace is reported and the run carries on: with a dozen Apps, one bad
|
|
163
174
|
* workspace must not cost the other eleven.
|
|
164
175
|
*/
|
|
165
|
-
async function syncTeam(rootDir, metadata, remote) {
|
|
176
|
+
async function syncTeam(rootDir, metadata, remote, source) {
|
|
166
177
|
const globalWorkspacePlan = planSync(metadata.globalWorkspaces, remote.globalWorkspaces);
|
|
167
178
|
const appPlan = planSync(metadata.apps, remote.apps);
|
|
168
179
|
const outcomes = [
|
|
169
180
|
...(await syncGlobalWorkspaces(rootDir, metadata, globalWorkspacePlan)),
|
|
170
|
-
...(await syncApps(rootDir, metadata, appPlan)),
|
|
181
|
+
...(await syncApps(rootDir, metadata, appPlan, source)),
|
|
171
182
|
];
|
|
172
183
|
const removedFolders = [
|
|
173
184
|
...globalWorkspacePlan.removed.map(({ recorded }) => (0, dev_team_1.globalWorkspaceFolder)(recorded.slug)),
|
|
@@ -185,6 +196,9 @@ function reportOutcomes({ outcomes, removedFolders }) {
|
|
|
185
196
|
else {
|
|
186
197
|
cli_1.default.log(color_1.default.green(`✅ ${outcome.folder} (${outcome.action})`));
|
|
187
198
|
}
|
|
199
|
+
for (const warning of outcome.warnings ?? []) {
|
|
200
|
+
cli_1.default.log(color_1.default.yellow(` ⚠️ ${warning}`));
|
|
201
|
+
}
|
|
188
202
|
}
|
|
189
203
|
for (const folder of removedFolders) {
|
|
190
204
|
cli_1.default.log(color_1.default.yellow(`⚠️ ${folder} is no longer in the team — the folder was kept, remove it yourself`));
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { GraphQLClient } from 'graphql-request';
|
|
2
2
|
import { Locale, Process, ProgrammingLanguage, ProgrammingLanguageManifest, RawTeamSettings, ScriptLibrary, TeamUpdateInput, TeamVersion, TeamVersionAlias, Variable, VersionedProcess, VersionedProgrammingLanguageManifest, VersionedScriptLibrary, ServiceAccount } from '../core/types';
|
|
3
|
+
/**
|
|
4
|
+
* What a delete mutation answers. `deletedAt` is set when the entity was kept as deleted because a
|
|
5
|
+
* workspace version still contains it; `retainingVersionTags` names those versions.
|
|
6
|
+
*/
|
|
7
|
+
export interface RemovedResource {
|
|
8
|
+
id: string;
|
|
9
|
+
deletedAt: string | null;
|
|
10
|
+
retainingVersionTags: string[];
|
|
11
|
+
}
|
|
3
12
|
export type FcodeGraphQLClientOptions = {
|
|
4
13
|
url: string;
|
|
5
14
|
accessToken: string;
|
|
@@ -16,10 +25,17 @@ declare class FcodeGraphQLClient {
|
|
|
16
25
|
cleanError(error: any): any;
|
|
17
26
|
private cloudUnavailableMessage;
|
|
18
27
|
private isNetworkError;
|
|
19
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Every process of the workspace, the deleted ones a workspace version still contains included:
|
|
30
|
+
* they are pulled read-only with their versions, so the local copy keeps what the versions run.
|
|
31
|
+
* An api that predates deleted-marked processes rejects the query over the unknown field, and
|
|
32
|
+
* the fallback lists the live ones as it always did.
|
|
33
|
+
*/
|
|
34
|
+
allProcesses(includeDeleted?: boolean): Promise<Array<Process>>;
|
|
20
35
|
allVersionedProcesses(processId: string): Promise<Array<VersionedProcess>>;
|
|
21
36
|
createVersionedProcesses(versionedProcess: VersionedProcess): Promise<void>;
|
|
22
|
-
|
|
37
|
+
/** Same deleted-inclusive listing, and the same fallback, as {@link allProcesses}. */
|
|
38
|
+
allScriptLibraries(includeDeleted?: boolean): Promise<Array<ScriptLibrary>>;
|
|
23
39
|
allVersionedScriptLibraries(scriptLibraryId: string): Promise<Array<VersionedScriptLibrary>>;
|
|
24
40
|
createVersionedScriptLibrary(versionedScriptLibrary: VersionedScriptLibrary): Promise<void>;
|
|
25
41
|
allVariables(): Promise<Array<Variable>>;
|
|
@@ -29,26 +45,29 @@ declare class FcodeGraphQLClient {
|
|
|
29
45
|
* kept twice here — an override is layered over the parent's file key by key, so the parent's
|
|
30
46
|
* keys are still what the ones it does not mention resolve to.
|
|
31
47
|
*/
|
|
32
|
-
allLocales(): Promise<Array<Locale>>;
|
|
48
|
+
allLocales(includeDeleted?: boolean): Promise<Array<Locale>>;
|
|
49
|
+
private fetchLocales;
|
|
33
50
|
pushLocale(locale: Locale): Promise<Locale>;
|
|
34
51
|
updateLocale(locale: Locale): Promise<Locale>;
|
|
35
|
-
removeLocale(locale: string | undefined): Promise<
|
|
52
|
+
removeLocale(locale: string | undefined): Promise<RemovedResource>;
|
|
36
53
|
private toProcessInput;
|
|
37
54
|
pushProcess(process: Process): Promise<Process>;
|
|
38
|
-
removeProcess(id: String | undefined): Promise<
|
|
55
|
+
removeProcess(id: String | undefined): Promise<RemovedResource>;
|
|
39
56
|
pushScriptLibrary(scriptLibrary: ScriptLibrary): Promise<ScriptLibrary>;
|
|
40
|
-
removeScriptLibrary(id: String | undefined): Promise<
|
|
57
|
+
removeScriptLibrary(id: String | undefined): Promise<RemovedResource>;
|
|
41
58
|
pushVariable(variable: Variable): Promise<Variable>;
|
|
42
59
|
updateVariable(variable: Variable): Promise<Variable>;
|
|
43
60
|
removeVariable(id: String | undefined): Promise<Process>;
|
|
44
61
|
programmingLanguageManifest(programmingLanguage: ProgrammingLanguage): Promise<ProgrammingLanguageManifest>;
|
|
45
62
|
private inheritedTeam;
|
|
63
|
+
private deletionOf;
|
|
64
|
+
private isUndefinedDeletionFieldError;
|
|
46
65
|
/**
|
|
47
66
|
* The public mode and the default header name are left out, so a public webhook — or one
|
|
48
67
|
* using the default header — keeps the metadata.json it already had on disk.
|
|
49
68
|
*/
|
|
50
69
|
private mapWebhook;
|
|
51
|
-
mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug }: any): Process;
|
|
70
|
+
mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug, deletedAt, retainingVersionTags }: any): Process;
|
|
52
71
|
/**
|
|
53
72
|
* GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
|
|
54
73
|
* keep it flat. Mapping it explicitly is what keeps that mismatch from silently reading as
|
|
@@ -33,6 +33,13 @@ const TEAM_VERSION_ALIAS_FIELDS = `
|
|
|
33
33
|
createdAt
|
|
34
34
|
updatedAt
|
|
35
35
|
`;
|
|
36
|
+
// What a list query adds to know a deleted resource a workspace version still contains. Asked
|
|
37
|
+
// for together with `includeDeleted`, and dropped as one when the api predates them.
|
|
38
|
+
const DELETION_FIELDS = `
|
|
39
|
+
deletedAt
|
|
40
|
+
retainingVersionTags
|
|
41
|
+
`;
|
|
42
|
+
const DELETION_QUERY_FIELDS = ['includeDeleted', 'deletedAt', 'retainingVersionTags'];
|
|
36
43
|
// Shared process selection so pull queries and push mutations return the same
|
|
37
44
|
// fields, keeping mapProcess (and therefore contentHash) consistent.
|
|
38
45
|
const PROCESS_FIELDS = `
|
|
@@ -154,24 +161,35 @@ class FcodeGraphQLClient {
|
|
|
154
161
|
const message = typeof error?.message === 'string' ? error.message : '';
|
|
155
162
|
return NETWORK_ERROR_CODES.some((code) => message.includes(code)) || message.includes('fetch failed');
|
|
156
163
|
}
|
|
157
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Every process of the workspace, the deleted ones a workspace version still contains included:
|
|
166
|
+
* they are pulled read-only with their versions, so the local copy keeps what the versions run.
|
|
167
|
+
* An api that predates deleted-marked processes rejects the query over the unknown field, and
|
|
168
|
+
* the fallback lists the live ones as it always did.
|
|
169
|
+
*/
|
|
170
|
+
allProcesses(includeDeleted = true) {
|
|
158
171
|
return this.client
|
|
159
172
|
.request((0, graphql_request_1.gql) `
|
|
160
|
-
query processes($first: Int!, $after: String) {
|
|
161
|
-
processes(first: $first, after: $after) {
|
|
173
|
+
query processes($filter: ProcessesFilter, $first: Int!, $after: String) {
|
|
174
|
+
processes(filter: $filter, first: $first, after: $after) {
|
|
162
175
|
edges {
|
|
163
176
|
node {
|
|
164
177
|
${PROCESS_FIELDS}
|
|
178
|
+
${includeDeleted ? DELETION_FIELDS : ''}
|
|
165
179
|
}
|
|
166
180
|
}
|
|
167
181
|
}
|
|
168
182
|
}
|
|
169
183
|
`, {
|
|
184
|
+
filter: includeDeleted ? { includeDeleted: true } : null,
|
|
170
185
|
after: null,
|
|
171
186
|
first: 200,
|
|
172
187
|
})
|
|
173
188
|
.then(({ processes: { edges } }) => edges.map(({ node: process }) => this.mapProcess(process)))
|
|
174
189
|
.catch((error) => {
|
|
190
|
+
if (includeDeleted && this.isUndefinedDeletionFieldError(error)) {
|
|
191
|
+
return this.allProcesses(false);
|
|
192
|
+
}
|
|
175
193
|
return Promise.reject(this.cleanError(error));
|
|
176
194
|
});
|
|
177
195
|
}
|
|
@@ -217,11 +235,12 @@ class FcodeGraphQLClient {
|
|
|
217
235
|
return Promise.reject(this.cleanError(error));
|
|
218
236
|
});
|
|
219
237
|
}
|
|
220
|
-
|
|
238
|
+
/** Same deleted-inclusive listing, and the same fallback, as {@link allProcesses}. */
|
|
239
|
+
allScriptLibraries(includeDeleted = true) {
|
|
221
240
|
return this.client
|
|
222
241
|
.request((0, graphql_request_1.gql) `
|
|
223
|
-
query scriptLibraries($first: Int!, $after: String) {
|
|
224
|
-
scriptLibraries(first: $first, after: $after) {
|
|
242
|
+
query scriptLibraries($filter: ScriptLibrariesFilter, $first: Int!, $after: String) {
|
|
243
|
+
scriptLibraries(filter: $filter, first: $first, after: $after) {
|
|
225
244
|
edges {
|
|
226
245
|
node {
|
|
227
246
|
id
|
|
@@ -229,16 +248,21 @@ class FcodeGraphQLClient {
|
|
|
229
248
|
sourceCode
|
|
230
249
|
programmingLanguage
|
|
231
250
|
resolvingTeamSlug
|
|
251
|
+
${includeDeleted ? DELETION_FIELDS : ''}
|
|
232
252
|
}
|
|
233
253
|
}
|
|
234
254
|
}
|
|
235
255
|
}
|
|
236
256
|
`, {
|
|
257
|
+
filter: includeDeleted ? { includeDeleted: true } : null,
|
|
237
258
|
after: null,
|
|
238
259
|
first: 200,
|
|
239
260
|
})
|
|
240
|
-
.then(({ scriptLibraries: { edges } }) => edges.map(({ node: { id, name, programmingLanguage, sourceCode, resolvingTeamSlug } }) => new types_1.ScriptLibrary(id, name, sourceCode || '', programmingLanguage, this.inheritedTeam(resolvingTeamSlug))))
|
|
261
|
+
.then(({ scriptLibraries: { edges } }) => edges.map(({ node: { id, name, programmingLanguage, sourceCode, resolvingTeamSlug, deletedAt, retainingVersionTags } }) => new types_1.ScriptLibrary(id, name, sourceCode || '', programmingLanguage, this.inheritedTeam(resolvingTeamSlug), this.deletionOf(deletedAt, retainingVersionTags))))
|
|
241
262
|
.catch((error) => {
|
|
263
|
+
if (includeDeleted && this.isUndefinedDeletionFieldError(error)) {
|
|
264
|
+
return this.allScriptLibraries(false);
|
|
265
|
+
}
|
|
242
266
|
return Promise.reject(this.cleanError(error));
|
|
243
267
|
});
|
|
244
268
|
}
|
|
@@ -325,40 +349,16 @@ class FcodeGraphQLClient {
|
|
|
325
349
|
* kept twice here — an override is layered over the parent's file key by key, so the parent's
|
|
326
350
|
* keys are still what the ones it does not mention resolve to.
|
|
327
351
|
*/
|
|
328
|
-
async allLocales() {
|
|
329
|
-
|
|
330
|
-
const locales = [];
|
|
331
|
-
let after = null;
|
|
352
|
+
async allLocales(includeDeleted = true) {
|
|
353
|
+
let locales;
|
|
332
354
|
try {
|
|
333
|
-
|
|
334
|
-
const { locales: { edges, pageInfo }, } = await this.client.request((0, graphql_request_1.gql) `
|
|
335
|
-
query locales($first: Int!, $after: String) {
|
|
336
|
-
locales(first: $first, after: $after) {
|
|
337
|
-
edges {
|
|
338
|
-
node {
|
|
339
|
-
id
|
|
340
|
-
locale
|
|
341
|
-
content
|
|
342
|
-
resolvingTeamSlug
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
pageInfo {
|
|
346
|
-
hasNextPage
|
|
347
|
-
endCursor
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
`, {
|
|
352
|
-
after,
|
|
353
|
-
first: pageSize,
|
|
354
|
-
});
|
|
355
|
-
for (const { node: { id, locale, content, resolvingTeamSlug }, } of edges) {
|
|
356
|
-
locales.push(new types_1.Locale(id, locale, content, this.inheritedTeam(resolvingTeamSlug)));
|
|
357
|
-
}
|
|
358
|
-
after = pageInfo?.hasNextPage ? pageInfo.endCursor : null;
|
|
359
|
-
} while (after);
|
|
355
|
+
locales = await this.fetchLocales(includeDeleted);
|
|
360
356
|
}
|
|
361
357
|
catch (error) {
|
|
358
|
+
// Same fallback as allProcesses: an api that predates deleted-marked locales lists the live ones.
|
|
359
|
+
if (includeDeleted && this.isUndefinedDeletionFieldError(error)) {
|
|
360
|
+
return this.allLocales(false);
|
|
361
|
+
}
|
|
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
|
|
@@ -368,6 +368,41 @@ class FcodeGraphQLClient {
|
|
|
368
368
|
(0, locale_merger_1.assertLocaleFilesCanCoexist)(merged);
|
|
369
369
|
return merged;
|
|
370
370
|
}
|
|
371
|
+
async fetchLocales(includeDeleted) {
|
|
372
|
+
const pageSize = 200;
|
|
373
|
+
const locales = [];
|
|
374
|
+
let after = null;
|
|
375
|
+
do {
|
|
376
|
+
const { locales: { edges, pageInfo }, } = await this.client.request((0, graphql_request_1.gql) `
|
|
377
|
+
query locales($filter: LocalesFilter, $first: Int!, $after: String) {
|
|
378
|
+
locales(filter: $filter, first: $first, after: $after) {
|
|
379
|
+
edges {
|
|
380
|
+
node {
|
|
381
|
+
id
|
|
382
|
+
locale
|
|
383
|
+
content
|
|
384
|
+
resolvingTeamSlug
|
|
385
|
+
${includeDeleted ? DELETION_FIELDS : ''}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
pageInfo {
|
|
389
|
+
hasNextPage
|
|
390
|
+
endCursor
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
`, {
|
|
395
|
+
filter: includeDeleted ? { includeDeleted: true } : null,
|
|
396
|
+
after,
|
|
397
|
+
first: pageSize,
|
|
398
|
+
});
|
|
399
|
+
for (const { node: { id, locale, content, resolvingTeamSlug, deletedAt, retainingVersionTags }, } of edges) {
|
|
400
|
+
locales.push(new types_1.Locale(id, locale, content, this.inheritedTeam(resolvingTeamSlug), this.deletionOf(deletedAt, retainingVersionTags)));
|
|
401
|
+
}
|
|
402
|
+
after = pageInfo?.hasNextPage ? pageInfo.endCursor : null;
|
|
403
|
+
} while (after);
|
|
404
|
+
return locales;
|
|
405
|
+
}
|
|
371
406
|
pushLocale(locale) {
|
|
372
407
|
return this.client
|
|
373
408
|
.request((0, graphql_request_1.gql) `
|
|
@@ -417,12 +452,14 @@ class FcodeGraphQLClient {
|
|
|
417
452
|
mutation deleteLocale($locale: String!) {
|
|
418
453
|
deleteLocale(locale: $locale) {
|
|
419
454
|
id
|
|
455
|
+
deletedAt
|
|
456
|
+
retainingVersionTags
|
|
420
457
|
}
|
|
421
458
|
}
|
|
422
459
|
`, {
|
|
423
460
|
locale,
|
|
424
461
|
})
|
|
425
|
-
.then(() =>
|
|
462
|
+
.then((data) => data.deleteLocale)
|
|
426
463
|
.catch((error) => {
|
|
427
464
|
return Promise.reject(this.cleanError(error));
|
|
428
465
|
});
|
|
@@ -531,11 +568,14 @@ class FcodeGraphQLClient {
|
|
|
531
568
|
mutation deleteProcess($id: ID!) {
|
|
532
569
|
deleteProcess(id: $id) {
|
|
533
570
|
id
|
|
571
|
+
deletedAt
|
|
572
|
+
retainingVersionTags
|
|
534
573
|
}
|
|
535
574
|
}
|
|
536
575
|
`, {
|
|
537
576
|
id,
|
|
538
577
|
})
|
|
578
|
+
.then((data) => data.deleteProcess)
|
|
539
579
|
.catch((error) => {
|
|
540
580
|
return Promise.reject(this.cleanError(error));
|
|
541
581
|
});
|
|
@@ -590,11 +630,14 @@ class FcodeGraphQLClient {
|
|
|
590
630
|
mutation deleteScriptLibrary($id: ID!) {
|
|
591
631
|
deleteScriptLibrary(id: $id) {
|
|
592
632
|
id
|
|
633
|
+
deletedAt
|
|
634
|
+
retainingVersionTags
|
|
593
635
|
}
|
|
594
636
|
}
|
|
595
637
|
`, {
|
|
596
638
|
id,
|
|
597
639
|
})
|
|
640
|
+
.then((data) => data.deleteScriptLibrary)
|
|
598
641
|
.catch((error) => {
|
|
599
642
|
return Promise.reject(this.cleanError(error));
|
|
600
643
|
});
|
|
@@ -725,6 +768,12 @@ class FcodeGraphQLClient {
|
|
|
725
768
|
inheritedTeam(resolvingTeamSlug) {
|
|
726
769
|
return resolvingTeamSlug && resolvingTeamSlug !== this.teamSlug ? resolvingTeamSlug : undefined;
|
|
727
770
|
}
|
|
771
|
+
deletionOf(deletedAt, retainingVersionTags) {
|
|
772
|
+
return deletedAt ? { deletedAt, retainingVersionTags: retainingVersionTags ?? [] } : undefined;
|
|
773
|
+
}
|
|
774
|
+
isUndefinedDeletionFieldError(error) {
|
|
775
|
+
return DELETION_QUERY_FIELDS.some((field) => this.isUndefinedFieldError(error, field));
|
|
776
|
+
}
|
|
728
777
|
/**
|
|
729
778
|
* The public mode and the default header name are left out, so a public webhook — or one
|
|
730
779
|
* using the default header — keeps the metadata.json it already had on disk.
|
|
@@ -747,7 +796,7 @@ class FcodeGraphQLClient {
|
|
|
747
796
|
}
|
|
748
797
|
return webhook;
|
|
749
798
|
}
|
|
750
|
-
mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug }) {
|
|
799
|
+
mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug, deletedAt, retainingVersionTags }) {
|
|
751
800
|
const dependencies = _.map(manifest?.dependencies, (dependency) => {
|
|
752
801
|
return { name: dependency.name, specifier: dependency.gitUrl ?? dependency.semVer };
|
|
753
802
|
});
|
|
@@ -783,7 +832,7 @@ class FcodeGraphQLClient {
|
|
|
783
832
|
metadata.uiTrigger.awaitResult = true;
|
|
784
833
|
}
|
|
785
834
|
}
|
|
786
|
-
return new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug));
|
|
835
|
+
return new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug), this.deletionOf(deletedAt, retainingVersionTags));
|
|
787
836
|
}
|
|
788
837
|
/**
|
|
789
838
|
* GraphQL nests the webhook configuration under `settings`, while settings.json and the REST API
|
|
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.installSkillsAt = installSkillsAt;
|
|
4
4
|
exports.skillsInstalledAt = skillsInstalledAt;
|
|
5
5
|
exports.linkSkillsInto = linkSkillsInto;
|
|
6
|
-
const node_child_process_1 = require("node:child_process");
|
|
7
6
|
const fs = require("fs-extra");
|
|
8
7
|
const path = require("node:path");
|
|
8
|
+
const child_process_1 = require("../utils/child-process");
|
|
9
9
|
const cli_1 = require("../utils/cli");
|
|
10
10
|
// Skill sources installed on clone. `release-sdk` in factorial-api-sdks is a
|
|
11
11
|
// maintainer-only skill, so we pick `factorial-api-sdks` explicitly there.
|
|
@@ -24,7 +24,7 @@ async function installSkills(repo, skills, cwd) {
|
|
|
24
24
|
// the skills CLI defaults: project scope (no -g), symlinked (no --copy).
|
|
25
25
|
const skillsArgs = ['--yes', 'skills', 'add', repo, '-s', skills, '-y'];
|
|
26
26
|
await new Promise((resolve) => {
|
|
27
|
-
const child = (0,
|
|
27
|
+
const child = (0, child_process_1.spawnCommand)('npx', skillsArgs, { cwd, stdio: 'inherit' });
|
|
28
28
|
child.on('error', () => {
|
|
29
29
|
// npx not available (or failed to launch) — don't fail the clone, just tell the user how to do it.
|
|
30
30
|
cli_1.default.log("\n⚠️ Couldn't run npx automatically (is Node.js / npx installed?).");
|