@factorialco/fcode-cli 3.2.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 (32) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/i18n/status.js +9 -0
  3. package/dist/commands/modules/pull.js +5 -4
  4. package/dist/commands/modules/push.js +1 -1
  5. package/dist/commands/modules/status.js +9 -0
  6. package/dist/commands/processes/pull.js +5 -4
  7. package/dist/commands/processes/push.js +1 -1
  8. package/dist/commands/processes/status.js +9 -0
  9. package/dist/commands/runtime-env.js +2 -2
  10. package/dist/core/commands/resource-command.d.ts +4 -1
  11. package/dist/core/commands/resource-command.js +9 -2
  12. package/dist/core/dependencies/javascript-env-dependencies.js +4 -4
  13. package/dist/core/dependencies/python-env-dependencies.js +5 -4
  14. package/dist/core/executors/execution-task.js +1 -1
  15. package/dist/core/executors/javascript-execution-task.js +21 -17
  16. package/dist/core/executors/python-execution-task.js +11 -10
  17. package/dist/core/resource-pusher.js +13 -7
  18. package/dist/core/types.d.ts +16 -3
  19. package/dist/core/types.js +31 -6
  20. package/dist/repositories/locale.js +5 -3
  21. package/dist/repositories/process.js +4 -2
  22. package/dist/repositories/script-library.js +3 -2
  23. package/dist/runtime/libs/python/fcode/datastore.py +3 -1
  24. package/dist/services/fcode-graphql-client.d.ts +26 -7
  25. package/dist/services/fcode-graphql-client.js +90 -41
  26. package/dist/services/skills-installer.js +2 -2
  27. package/dist/services/version-pull-reporter.d.ts +20 -0
  28. package/dist/services/version-pull-reporter.js +33 -0
  29. package/dist/utils/child-process.d.ts +26 -0
  30. package/dist/utils/child-process.js +60 -0
  31. package/oclif.manifest.json +1 -1
  32. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ServiceAccount = exports.Dependency = exports.DependenciesFlags = exports.TeamNameArg = exports.ShowInheritedFlag = exports.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.WorkspaceSettings = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedProgrammingLanguageManifest = exports.VersionedScriptLibrary = exports.Locale = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = exports.PROGRAMMING_LANGUAGES = exports.ResourceStatus = exports.OnConflictResolution = exports.Resource = void 0;
3
+ exports.ServiceAccount = exports.Dependency = exports.DependenciesFlags = exports.TeamNameArg = exports.ShowInheritedFlag = exports.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.WorkspaceSettings = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedProgrammingLanguageManifest = exports.VersionedScriptLibrary = exports.Locale = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = exports.PROGRAMMING_LANGUAGES = exports.ResourceStatus = exports.OnConflictResolution = exports.readOnlyReason = exports.isReadOnly = exports.Resource = void 0;
4
4
  const core_1 = require("@oclif/core");
5
5
  const crypto = require("node:crypto");
6
6
  const canonical_hash_1 = require("../utils/canonical-hash");
@@ -10,6 +10,27 @@ class Resource {
10
10
  }
11
11
  }
12
12
  exports.Resource = Resource;
13
+ /** Whether the workspace may edit the resource: neither a parent's nor a deleted one is its to change. */
14
+ const isReadOnly = (resource) => Boolean(resource.resolvingTeamSlug || resource.deletion);
15
+ exports.isReadOnly = isReadOnly;
16
+ /** The sentence `push` and `diff` use to say why a resource is not the workspace's to change. */
17
+ const readOnlyReason = (resource) => {
18
+ if (resource.resolvingTeamSlug) {
19
+ return `inherited from team '${resource.resolvingTeamSlug}'`;
20
+ }
21
+ if (resource.deletion) {
22
+ return `deleted in the cloud and kept by workspace version(s) ${resource.deletion.retainingVersionTags.join(', ')}`;
23
+ }
24
+ return undefined;
25
+ };
26
+ exports.readOnlyReason = readOnlyReason;
27
+ /**
28
+ * The deletion as it enters a content hash. A deleted resource must read as changed the moment the
29
+ * cloud deletes it — or brings it back — so a pull marks its files read-only (or writable again)
30
+ * even though their content did not move; a live resource contributes nothing, so no stored hash
31
+ * changes for it.
32
+ */
33
+ const deletionHashPart = (deletion) => deletion ? JSON.stringify({ deletedAt: deletion.deletedAt, retainingVersionTags: [...deletion.retainingVersionTags].sort() }) : '';
13
34
  var OnConflictResolution;
14
35
  (function (OnConflictResolution) {
15
36
  OnConflictResolution[OnConflictResolution["KEEP_LOCAL"] = 0] = "KEEP_LOCAL";
@@ -41,10 +62,11 @@ exports.programmingLanguageDisplayName = programmingLanguageDisplayName;
41
62
  // The header assumed when none is configured; its value carries a Bearer scheme.
42
63
  exports.DEFAULT_WEBHOOK_AUTH_HEADER = 'Authorization';
43
64
  class Process extends Resource {
44
- constructor(id, slug, script, readme, manifest, settings, metadata, resolvingTeamSlug) {
65
+ constructor(id, slug, script, readme, manifest, settings, metadata, resolvingTeamSlug, deletion) {
45
66
  super();
46
67
  this.id = id;
47
68
  this.slug = slug;
69
+ this.deletion = deletion;
48
70
  this.name = metadata.name;
49
71
  this.description = metadata.description;
50
72
  this.tags = metadata.tags;
@@ -72,6 +94,7 @@ class Process extends Resource {
72
94
  ...(this.uiTrigger && { uiTrigger: this.uiTrigger }),
73
95
  manifestDependencies: this.manifest.dependencies,
74
96
  dependenciesScopedToProcess: this.settings.dependencies.scopedToProcess,
97
+ ...(this.deletion && { deletion: deletionHashPart(this.deletion) }),
75
98
  });
76
99
  }
77
100
  }
@@ -94,7 +117,7 @@ class VersionedProcess extends Resource {
94
117
  }
95
118
  exports.VersionedProcess = VersionedProcess;
96
119
  class ScriptLibrary extends Resource {
97
- constructor(id, name, sourceCode, programmingLanguage, resolvingTeamSlug) {
120
+ constructor(id, name, sourceCode, programmingLanguage, resolvingTeamSlug, deletion) {
98
121
  super();
99
122
  this.id = id;
100
123
  this.slug = name;
@@ -102,20 +125,22 @@ class ScriptLibrary extends Resource {
102
125
  this.sourceCode = sourceCode;
103
126
  this.programmingLanguage = programmingLanguage;
104
127
  this.resolvingTeamSlug = resolvingTeamSlug;
128
+ this.deletion = deletion;
105
129
  }
106
130
  get contentHash() {
107
- return crypto.createHash('md5').update(this.sourceCode).digest('hex');
131
+ return crypto.createHash('md5').update(this.sourceCode).update(deletionHashPart(this.deletion)).digest('hex');
108
132
  }
109
133
  }
110
134
  exports.ScriptLibrary = ScriptLibrary;
111
135
  class Locale extends Resource {
112
- constructor(id, locale, content, resolvingTeamSlug) {
136
+ constructor(id, locale, content, resolvingTeamSlug, deletion) {
113
137
  super();
114
138
  this.id = id;
115
139
  this.slug = Locale.slugFor(locale, resolvingTeamSlug);
116
140
  this.locale = locale;
117
141
  this.content = content;
118
142
  this.resolvingTeamSlug = resolvingTeamSlug;
143
+ this.deletion = deletion;
119
144
  }
120
145
  /**
121
146
  * The name a locale is tracked and stored under. Unlike every other resource, the identifier is
@@ -133,7 +158,7 @@ class Locale extends Resource {
133
158
  // Hashed over the raw YAML: the file is the source of truth and round-trips comments and
134
159
  // ordering, so any edit — even one that parses to the same translations — is a change to push.
135
160
  get contentHash() {
136
- return crypto.createHash('md5').update(this.content).digest('hex');
161
+ return crypto.createHash('md5').update(this.content).update(deletionHashPart(this.deletion)).digest('hex');
137
162
  }
138
163
  }
139
164
  exports.Locale = Locale;
@@ -32,7 +32,7 @@ class LocaleRepository extends resource_1.default {
32
32
  }
33
33
  try {
34
34
  const content = fs.readFileSync(this.getResourcePath(localeMetadata)).toString();
35
- return new types_1.Locale(localeMetadata.id, localeMetadata.locale ?? slug, content, localeMetadata.resolvingTeamSlug);
35
+ return new types_1.Locale(localeMetadata.id, localeMetadata.locale ?? slug, content, localeMetadata.resolvingTeamSlug, localeMetadata.deletion);
36
36
  }
37
37
  catch (error) {
38
38
  throw new errors_1.LocalLocaleNotFound(error, slug);
@@ -78,12 +78,14 @@ class LocaleRepository extends resource_1.default {
78
78
  locale: locale.locale,
79
79
  slug: locale.slug,
80
80
  ...(locale.resolvingTeamSlug && { resolvingTeamSlug: locale.resolvingTeamSlug }),
81
+ ...(locale.deletion && { deletion: locale.deletion }),
81
82
  };
82
83
  }
83
84
  async saveFiles(locale) {
84
85
  // Inherited locales are written read-only, like inherited modules: they belong to a parent
85
- // workspace, and editing one here would push an override rather than change the parent.
86
- (0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(locale), locale.content || '', { readOnly: Boolean(locale.resolvingTeamSlug) });
86
+ // workspace, and editing one here would push an override rather than change the parent. A
87
+ // deleted locale is read-only too: only its versions are still resolved.
88
+ (0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(locale), locale.content || '', { readOnly: (0, types_1.isReadOnly)(locale) });
87
89
  }
88
90
  async metadataFromFiles(slug) {
89
91
  return {
@@ -147,7 +147,7 @@ class ProcessRepository extends resource_1.default {
147
147
  const manifest = isDependenciesFilePresent
148
148
  ? { dependencies: (0, dependencies_1.dependenciesFromFile)(processMetadata.script.programmingLanguage, fs.readFileSync(dependenciesFile).toString()) }
149
149
  : null;
150
- return new types_1.Process(processMetadata.id, processMetadata.slug, { parametersSchema, programmingLanguage: processMetadata.script.programmingLanguage, sourceCode }, readme, manifest, { dependencies: { scopedToProcess: isDependenciesFilePresent } }, metadata, processMetadata.resolvingTeamSlug);
150
+ return new types_1.Process(processMetadata.id, processMetadata.slug, { parametersSchema, programmingLanguage: processMetadata.script.programmingLanguage, sourceCode }, readme, manifest, { dependencies: { scopedToProcess: isDependenciesFilePresent } }, metadata, processMetadata.resolvingTeamSlug, processMetadata.deletion);
151
151
  }
152
152
  catch {
153
153
  throw new Error(`Process ${processSlug} not found!`);
@@ -229,6 +229,7 @@ class ProcessRepository extends resource_1.default {
229
229
  programmingLanguage: process.script.programmingLanguage,
230
230
  },
231
231
  ...(process.resolvingTeamSlug && { resolvingTeamSlug: process.resolvingTeamSlug }),
232
+ ...(process.deletion && { deletion: process.deletion }),
232
233
  };
233
234
  }
234
235
  async metadataFromFiles(slug) {
@@ -243,7 +244,8 @@ class ProcessRepository extends resource_1.default {
243
244
  };
244
245
  }
245
246
  async saveFiles(process) {
246
- const readOnly = Boolean(process.resolvingTeamSlug);
247
+ // A deleted process is kept exactly as its versions left it, like an inherited one: read-only.
248
+ const readOnly = (0, types_1.isReadOnly)(process);
247
249
  (0, file_utils_1.outputFileSyncIfChanged)(this.buildResourcePath(process, this.getSourceCodeFileName(process.script.programmingLanguage)), process.script.sourceCode || '', {
248
250
  readOnly,
249
251
  });
@@ -25,7 +25,7 @@ class ScriptLibraryRepository extends resource_1.default {
25
25
  const sourceCodeFile = fs.readFileSync(this.getResourcePath(scriptLibraryMetadata));
26
26
  try {
27
27
  const sourceCode = sourceCodeFile.toString();
28
- return new types_1.ScriptLibrary(scriptLibraryMetadata.id, scriptLibraryMetadata.name, sourceCode, scriptLibraryMetadata.programmingLanguage, scriptLibraryMetadata.resolvingTeamSlug);
28
+ return new types_1.ScriptLibrary(scriptLibraryMetadata.id, scriptLibraryMetadata.name, sourceCode, scriptLibraryMetadata.programmingLanguage, scriptLibraryMetadata.resolvingTeamSlug, scriptLibraryMetadata.deletion);
29
29
  }
30
30
  catch (error) {
31
31
  throw new errors_1.LocalScriptLibraryNotFound(error, slug);
@@ -83,10 +83,11 @@ class ScriptLibraryRepository extends resource_1.default {
83
83
  slug: scriptLibrary.slug,
84
84
  programmingLanguage: scriptLibrary.programmingLanguage,
85
85
  ...(scriptLibrary.resolvingTeamSlug && { resolvingTeamSlug: scriptLibrary.resolvingTeamSlug }),
86
+ ...(scriptLibrary.deletion && { deletion: scriptLibrary.deletion }),
86
87
  };
87
88
  }
88
89
  async saveFiles(scriptLibrary) {
89
- (0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(scriptLibrary), scriptLibrary.sourceCode || '', { readOnly: Boolean(scriptLibrary.resolvingTeamSlug) });
90
+ (0, file_utils_1.outputFileSyncIfChanged)(this.getResourcePath(scriptLibrary), scriptLibrary.sourceCode || '', { readOnly: (0, types_1.isReadOnly)(scriptLibrary) });
90
91
  }
91
92
  versionsDirectory(scriptLibrary) {
92
93
  return path.join(this._repositoryName, this.resourceDirectoryName(scriptLibrary), 'versions');
@@ -21,7 +21,9 @@ class Datastore:
21
21
  return self.getAll().get(key, None)
22
22
  raise TypeError("Invalid argument type")
23
23
 
24
- def set(self, key, value):
24
+ def set(self, key, value, encrypted=False):
25
+ if not isinstance(encrypted, bool):
26
+ raise TypeError("Invalid encrypted flag. Encrypted must be a boolean.")
25
27
  if isinstance(key, (str, int)) and isinstance(value, (str, int)):
26
28
  datastore = self.getAll()
27
29
  datastore[key] = value
@@ -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?).");
@@ -0,0 +1,20 @@
1
+ /** One resource's version pull, named so a failure can say which resource it belongs to. */
2
+ export interface VersionPull {
3
+ slug: string;
4
+ failure: Promise<string | undefined>;
5
+ }
6
+ /**
7
+ * Starts tracking one resource's version pull.
8
+ *
9
+ * The outcome is captured here rather than where it is reported, because the pulls are started in
10
+ * a loop that waits between them: a rejection would otherwise sit unhandled until the loop ends.
11
+ */
12
+ export declare function trackVersionPull(slug: string, done: Promise<void>): VersionPull;
13
+ /**
14
+ * Awaits every version pull and reports the ones that failed.
15
+ *
16
+ * The pulls run in parallel and must not take each other down — one resource the cloud refuses is
17
+ * not a reason to abandon the rest — but a swallowed rejection leaves `versions/` silently empty,
18
+ * which reads exactly like a resource that has no versions. This is what tells the two apart.
19
+ */
20
+ export declare function reportFailedVersionPulls(pulls: Array<VersionPull>, resourceName: string): Promise<void>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trackVersionPull = trackVersionPull;
4
+ exports.reportFailedVersionPulls = reportFailedVersionPulls;
5
+ const color_1 = require("@oclif/color");
6
+ const cli_1 = require("../utils/cli");
7
+ /**
8
+ * Starts tracking one resource's version pull.
9
+ *
10
+ * The outcome is captured here rather than where it is reported, because the pulls are started in
11
+ * a loop that waits between them: a rejection would otherwise sit unhandled until the loop ends.
12
+ */
13
+ function trackVersionPull(slug, done) {
14
+ return {
15
+ slug,
16
+ failure: done.then(() => undefined, (reason) => (reason instanceof Error ? reason.message : String(reason))),
17
+ };
18
+ }
19
+ /**
20
+ * Awaits every version pull and reports the ones that failed.
21
+ *
22
+ * The pulls run in parallel and must not take each other down — one resource the cloud refuses is
23
+ * not a reason to abandon the rest — but a swallowed rejection leaves `versions/` silently empty,
24
+ * which reads exactly like a resource that has no versions. This is what tells the two apart.
25
+ */
26
+ async function reportFailedVersionPulls(pulls, resourceName) {
27
+ for (const { slug, failure } of pulls) {
28
+ const reason = await failure;
29
+ if (reason !== undefined) {
30
+ cli_1.default.log(color_1.default.yellow(`⚠️ could not pull the versions of ${resourceName} '${slug}': ${reason}`));
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,26 @@
1
+ import { type ChildProcess, type SpawnOptions } from 'node:child_process';
2
+ export type Platform = NodeJS.Platform;
3
+ export interface CommandLine {
4
+ command: string;
5
+ args: string[];
6
+ shell: boolean;
7
+ }
8
+ /**
9
+ * How to invoke a package-manager executable (`npm`, `npx`, a `node_modules/.bin` shim) on `platform`.
10
+ *
11
+ * On Windows those are `.cmd` batch files, which Node refuses to spawn without a shell, so the
12
+ * command line goes through `cmd.exe` with every part quoted for it.
13
+ */
14
+ export declare function commandLineFor(command: string, args: string[], platform?: Platform): CommandLine;
15
+ export declare function spawnCommand(command: string, args: string[], options?: SpawnOptions): ChildProcess;
16
+ /** The `node_modules/.bin` shim `name` resolves to on `platform`. */
17
+ export declare function binShimPath(nodeModulesPath: string, name: string, platform?: Platform): string;
18
+ /** The executable `name` a Python virtual environment at `envPath` exposes on `platform`. */
19
+ export declare function venvExecutablePath(envPath: string, name: string, platform?: Platform): string;
20
+ /** The Python interpreter of the virtual environment at `envPath`; Windows venvs ship no `python3` alias. */
21
+ export declare function venvPythonPath(envPath: string, platform?: Platform): string;
22
+ /**
23
+ * An environment for a process run locally: the entries given plus, on Windows, the system
24
+ * variables Node and Python cannot start without (`SYSTEMROOT`, `TEMP`, ...).
25
+ */
26
+ export declare function childEnv(entries: Record<string, string | undefined>, platform?: Platform, hostEnv?: NodeJS.ProcessEnv): Record<string, string>;