@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.
Files changed (51) hide show
  1. package/README.md +24 -2
  2. package/dist/commands/app/push.d.ts +13 -0
  3. package/dist/commands/app/push.js +147 -0
  4. package/dist/commands/i18n/status.js +9 -0
  5. package/dist/commands/login.js +2 -1
  6. package/dist/commands/modules/pull.js +5 -4
  7. package/dist/commands/modules/push.js +1 -1
  8. package/dist/commands/modules/status.js +9 -0
  9. package/dist/commands/processes/pull.js +5 -4
  10. package/dist/commands/processes/push.js +1 -1
  11. package/dist/commands/processes/status.js +9 -0
  12. package/dist/commands/runtime-env.js +2 -2
  13. package/dist/commands/team/clone.js +1 -1
  14. package/dist/commands/team/pull.js +3 -2
  15. package/dist/commands/team/status.d.ts +2 -0
  16. package/dist/commands/team/status.js +44 -2
  17. package/dist/config/dev-team.d.ts +15 -0
  18. package/dist/config/dev-team.js +54 -1
  19. package/dist/config/settings.d.ts +1 -0
  20. package/dist/config/settings.js +6 -0
  21. package/dist/core/commands/resource-command.d.ts +4 -1
  22. package/dist/core/commands/resource-command.js +9 -2
  23. package/dist/core/dependencies/javascript-env-dependencies.js +4 -4
  24. package/dist/core/dependencies/python-env-dependencies.js +5 -4
  25. package/dist/core/executors/execution-task.js +1 -1
  26. package/dist/core/executors/javascript-execution-task.js +21 -17
  27. package/dist/core/executors/python-execution-task.js +11 -10
  28. package/dist/core/resource-pusher.js +13 -7
  29. package/dist/core/types.d.ts +16 -3
  30. package/dist/core/types.js +31 -6
  31. package/dist/hooks/init/auth.d.ts +19 -0
  32. package/dist/hooks/init/auth.js +40 -6
  33. package/dist/repositories/locale.js +5 -3
  34. package/dist/repositories/process.js +4 -2
  35. package/dist/repositories/script-library.js +3 -2
  36. package/dist/runtime/libs/python/fcode/datastore.py +3 -1
  37. package/dist/services/app-metadata-files.d.ts +98 -0
  38. package/dist/services/app-metadata-files.js +363 -0
  39. package/dist/services/dashboard-client.d.ts +59 -0
  40. package/dist/services/dashboard-client.js +116 -10
  41. package/dist/services/dev-team-service.d.ts +4 -1
  42. package/dist/services/dev-team-service.js +31 -17
  43. package/dist/services/fcode-graphql-client.d.ts +26 -7
  44. package/dist/services/fcode-graphql-client.js +90 -41
  45. package/dist/services/skills-installer.js +2 -2
  46. package/dist/services/version-pull-reporter.d.ts +20 -0
  47. package/dist/services/version-pull-reporter.js +33 -0
  48. package/dist/utils/child-process.d.ts +26 -0
  49. package/dist/utils/child-process.js +60 -0
  50. package/oclif.manifest.json +35 -2
  51. 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(({ id, name, description }) => ({ id, name, description }));
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
- const url = `${this.baseUrl}${path}?${new URLSearchParams(query).toString()}`;
80
- let response = await (0, node_fetch_1.default)(url, { headers: await this.headers() });
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, { headers: await this.headers() });
186
+ response = await (0, node_fetch_1.default)(url, await init());
86
187
  }
87
188
  }
88
189
  if (!response.ok) {
89
- throw new Error(`Dashboard API ${response.status} on ${path}: ${await this.errorDetail(response)}`);
190
+ const { detail, code } = await this.errorDetail(response);
191
+ throw new DashboardApiError(response.status, path, detail, code);
90
192
  }
91
- return (await response.json());
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 { accept: 'application/json', authorization: `Bearer ${token.access_token}` };
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
- * Clones one App. The metadata entry is recorded as soon as the directory exists, before the
65
- * pull that may fail: a half-cloned App has to be recognised as cloned next time, or the retry
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(rootDir, app, folder, metadata) {
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
- metadata.apps[app.id] = { name: app.name, description: app.description, folder, devWorkspaceSlug: slug };
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(rootDir, cloudUrl, app, recorded) {
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 — pushing happens inside a workspace.
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(rootDir, app, folder, metadata);
122
- outcomes.push({ name: app.name, folder, action: 'cloned' });
130
+ await cloneApp(context, app, folder, warnings);
123
131
  }
124
132
  catch (error) {
125
- outcomes.push({ name: app.name, folder, action: 'cloned', error: error });
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(rootDir, metadata.cloudUrl, app, recorded);
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
- outcomes.push({ name: app.name, folder: recorded.folder, action: 'pulled', error: error });
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
- allProcesses(): Promise<Array<Process>>;
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
- allScriptLibraries(): Promise<Array<ScriptLibrary>>;
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<void>;
52
+ removeLocale(locale: string | undefined): Promise<RemovedResource>;
36
53
  private toProcessInput;
37
54
  pushProcess(process: Process): Promise<Process>;
38
- removeProcess(id: String | undefined): Promise<Process>;
55
+ removeProcess(id: String | undefined): Promise<RemovedResource>;
39
56
  pushScriptLibrary(scriptLibrary: ScriptLibrary): Promise<ScriptLibrary>;
40
- removeScriptLibrary(id: String | undefined): Promise<Process>;
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
- allProcesses() {
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
- allScriptLibraries() {
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
- const pageSize = 200;
330
- const locales = [];
331
- let after = null;
352
+ async allLocales(includeDeleted = true) {
353
+ let locales;
332
354
  try {
333
- do {
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(() => undefined)
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, node_child_process_1.spawn)('npx', skillsArgs, { cwd, stdio: 'inherit' });
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?).");