@factorialco/fcode-cli 3.2.0 → 3.4.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 +1 -1
- package/dist/commands/i18n/status.js +9 -0
- 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/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 +76 -3
- package/dist/core/types.js +33 -6
- package/dist/repositories/locale.js +5 -3
- package/dist/repositories/process.js +20 -2
- package/dist/repositories/script-library.js +3 -2
- package/dist/runtime/libs/python/fcode/datastore.py +3 -1
- package/dist/services/fcode-graphql-client.d.ts +45 -7
- package/dist/services/fcode-graphql-client.js +264 -46
- 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 +1 -1
- package/package.json +1 -1
|
@@ -33,6 +33,24 @@ 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'];
|
|
43
|
+
// The optional fields of the agent tool contract, by kind. All are written to metadata.json only
|
|
44
|
+
// when present. On push a list is always sent and a clearable text always sent as at least "",
|
|
45
|
+
// since the platform clears both on an empty value; `effect` is the one text it does not clear,
|
|
46
|
+
// so that one is sent only when present.
|
|
47
|
+
const AGENT_TOOL_CLEARABLE_TEXT_FIELDS = ['description', 'whenToUse', 'degradation', 'simulatesFor'];
|
|
48
|
+
const AGENT_TOOL_TEXT_FIELDS = [...AGENT_TOOL_CLEARABLE_TEXT_FIELDS, 'effect'];
|
|
49
|
+
const AGENT_TOOL_LIST_FIELDS = ['whenNotToUse', 'preconditions', 'doesNotDo'];
|
|
50
|
+
/** A settings block that is disabled and holds nothing else is omitted from metadata.json. */
|
|
51
|
+
function unlessAllDefaults(block) {
|
|
52
|
+
return !block.enabled && Object.keys(block).length === 1 ? undefined : block;
|
|
53
|
+
}
|
|
36
54
|
// Shared process selection so pull queries and push mutations return the same
|
|
37
55
|
// fields, keeping mapProcess (and therefore contentHash) consistent.
|
|
38
56
|
const PROCESS_FIELDS = `
|
|
@@ -42,6 +60,7 @@ const PROCESS_FIELDS = `
|
|
|
42
60
|
description
|
|
43
61
|
tags
|
|
44
62
|
resolvingTeamSlug
|
|
63
|
+
lifecycleRole
|
|
45
64
|
script {
|
|
46
65
|
parametersSchema
|
|
47
66
|
sourceCode
|
|
@@ -78,6 +97,36 @@ const PROCESS_FIELDS = `
|
|
|
78
97
|
icon
|
|
79
98
|
awaitResult
|
|
80
99
|
}
|
|
100
|
+
factorial {
|
|
101
|
+
enabled
|
|
102
|
+
awaitResult
|
|
103
|
+
requiredPolicies
|
|
104
|
+
uiTrigger {
|
|
105
|
+
enabled
|
|
106
|
+
locationId
|
|
107
|
+
label
|
|
108
|
+
icon
|
|
109
|
+
}
|
|
110
|
+
form {
|
|
111
|
+
enabled
|
|
112
|
+
public
|
|
113
|
+
appTool
|
|
114
|
+
}
|
|
115
|
+
agentTool {
|
|
116
|
+
enabled
|
|
117
|
+
description
|
|
118
|
+
effect
|
|
119
|
+
whenToUse
|
|
120
|
+
whenNotToUse
|
|
121
|
+
preconditions
|
|
122
|
+
doesNotDo
|
|
123
|
+
degradation
|
|
124
|
+
simulatesFor
|
|
125
|
+
}
|
|
126
|
+
backend {
|
|
127
|
+
enabled
|
|
128
|
+
}
|
|
129
|
+
}
|
|
81
130
|
dependencies {
|
|
82
131
|
scopedToProcess
|
|
83
132
|
}
|
|
@@ -154,24 +203,35 @@ class FcodeGraphQLClient {
|
|
|
154
203
|
const message = typeof error?.message === 'string' ? error.message : '';
|
|
155
204
|
return NETWORK_ERROR_CODES.some((code) => message.includes(code)) || message.includes('fetch failed');
|
|
156
205
|
}
|
|
157
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Every process of the workspace, the deleted ones a workspace version still contains included:
|
|
208
|
+
* they are pulled read-only with their versions, so the local copy keeps what the versions run.
|
|
209
|
+
* An api that predates deleted-marked processes rejects the query over the unknown field, and
|
|
210
|
+
* the fallback lists the live ones as it always did.
|
|
211
|
+
*/
|
|
212
|
+
allProcesses(includeDeleted = true) {
|
|
158
213
|
return this.client
|
|
159
214
|
.request((0, graphql_request_1.gql) `
|
|
160
|
-
query processes($first: Int!, $after: String) {
|
|
161
|
-
processes(first: $first, after: $after) {
|
|
215
|
+
query processes($filter: ProcessesFilter, $first: Int!, $after: String) {
|
|
216
|
+
processes(filter: $filter, first: $first, after: $after) {
|
|
162
217
|
edges {
|
|
163
218
|
node {
|
|
164
219
|
${PROCESS_FIELDS}
|
|
220
|
+
${includeDeleted ? DELETION_FIELDS : ''}
|
|
165
221
|
}
|
|
166
222
|
}
|
|
167
223
|
}
|
|
168
224
|
}
|
|
169
225
|
`, {
|
|
226
|
+
filter: includeDeleted ? { includeDeleted: true } : null,
|
|
170
227
|
after: null,
|
|
171
228
|
first: 200,
|
|
172
229
|
})
|
|
173
230
|
.then(({ processes: { edges } }) => edges.map(({ node: process }) => this.mapProcess(process)))
|
|
174
231
|
.catch((error) => {
|
|
232
|
+
if (includeDeleted && this.isUndefinedDeletionFieldError(error)) {
|
|
233
|
+
return this.allProcesses(false);
|
|
234
|
+
}
|
|
175
235
|
return Promise.reject(this.cleanError(error));
|
|
176
236
|
});
|
|
177
237
|
}
|
|
@@ -217,11 +277,12 @@ class FcodeGraphQLClient {
|
|
|
217
277
|
return Promise.reject(this.cleanError(error));
|
|
218
278
|
});
|
|
219
279
|
}
|
|
220
|
-
|
|
280
|
+
/** Same deleted-inclusive listing, and the same fallback, as {@link allProcesses}. */
|
|
281
|
+
allScriptLibraries(includeDeleted = true) {
|
|
221
282
|
return this.client
|
|
222
283
|
.request((0, graphql_request_1.gql) `
|
|
223
|
-
query scriptLibraries($first: Int!, $after: String) {
|
|
224
|
-
scriptLibraries(first: $first, after: $after) {
|
|
284
|
+
query scriptLibraries($filter: ScriptLibrariesFilter, $first: Int!, $after: String) {
|
|
285
|
+
scriptLibraries(filter: $filter, first: $first, after: $after) {
|
|
225
286
|
edges {
|
|
226
287
|
node {
|
|
227
288
|
id
|
|
@@ -229,16 +290,21 @@ class FcodeGraphQLClient {
|
|
|
229
290
|
sourceCode
|
|
230
291
|
programmingLanguage
|
|
231
292
|
resolvingTeamSlug
|
|
293
|
+
${includeDeleted ? DELETION_FIELDS : ''}
|
|
232
294
|
}
|
|
233
295
|
}
|
|
234
296
|
}
|
|
235
297
|
}
|
|
236
298
|
`, {
|
|
299
|
+
filter: includeDeleted ? { includeDeleted: true } : null,
|
|
237
300
|
after: null,
|
|
238
301
|
first: 200,
|
|
239
302
|
})
|
|
240
|
-
.then(({ scriptLibraries: { edges } }) => edges.map(({ node: { id, name, programmingLanguage, sourceCode, resolvingTeamSlug } }) => new types_1.ScriptLibrary(id, name, sourceCode || '', programmingLanguage, this.inheritedTeam(resolvingTeamSlug))))
|
|
303
|
+
.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
304
|
.catch((error) => {
|
|
305
|
+
if (includeDeleted && this.isUndefinedDeletionFieldError(error)) {
|
|
306
|
+
return this.allScriptLibraries(false);
|
|
307
|
+
}
|
|
242
308
|
return Promise.reject(this.cleanError(error));
|
|
243
309
|
});
|
|
244
310
|
}
|
|
@@ -325,40 +391,16 @@ class FcodeGraphQLClient {
|
|
|
325
391
|
* kept twice here — an override is layered over the parent's file key by key, so the parent's
|
|
326
392
|
* keys are still what the ones it does not mention resolve to.
|
|
327
393
|
*/
|
|
328
|
-
async allLocales() {
|
|
329
|
-
|
|
330
|
-
const locales = [];
|
|
331
|
-
let after = null;
|
|
394
|
+
async allLocales(includeDeleted = true) {
|
|
395
|
+
let locales;
|
|
332
396
|
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);
|
|
397
|
+
locales = await this.fetchLocales(includeDeleted);
|
|
360
398
|
}
|
|
361
399
|
catch (error) {
|
|
400
|
+
// Same fallback as allProcesses: an api that predates deleted-marked locales lists the live ones.
|
|
401
|
+
if (includeDeleted && this.isUndefinedDeletionFieldError(error)) {
|
|
402
|
+
return this.allLocales(false);
|
|
403
|
+
}
|
|
362
404
|
return Promise.reject(this.cleanError(error));
|
|
363
405
|
}
|
|
364
406
|
// The cloud team's parent order is the inheritance precedence the merge must replicate; the
|
|
@@ -368,6 +410,41 @@ class FcodeGraphQLClient {
|
|
|
368
410
|
(0, locale_merger_1.assertLocaleFilesCanCoexist)(merged);
|
|
369
411
|
return merged;
|
|
370
412
|
}
|
|
413
|
+
async fetchLocales(includeDeleted) {
|
|
414
|
+
const pageSize = 200;
|
|
415
|
+
const locales = [];
|
|
416
|
+
let after = null;
|
|
417
|
+
do {
|
|
418
|
+
const { locales: { edges, pageInfo }, } = await this.client.request((0, graphql_request_1.gql) `
|
|
419
|
+
query locales($filter: LocalesFilter, $first: Int!, $after: String) {
|
|
420
|
+
locales(filter: $filter, first: $first, after: $after) {
|
|
421
|
+
edges {
|
|
422
|
+
node {
|
|
423
|
+
id
|
|
424
|
+
locale
|
|
425
|
+
content
|
|
426
|
+
resolvingTeamSlug
|
|
427
|
+
${includeDeleted ? DELETION_FIELDS : ''}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
pageInfo {
|
|
431
|
+
hasNextPage
|
|
432
|
+
endCursor
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
`, {
|
|
437
|
+
filter: includeDeleted ? { includeDeleted: true } : null,
|
|
438
|
+
after,
|
|
439
|
+
first: pageSize,
|
|
440
|
+
});
|
|
441
|
+
for (const { node: { id, locale, content, resolvingTeamSlug, deletedAt, retainingVersionTags }, } of edges) {
|
|
442
|
+
locales.push(new types_1.Locale(id, locale, content, this.inheritedTeam(resolvingTeamSlug), this.deletionOf(deletedAt, retainingVersionTags)));
|
|
443
|
+
}
|
|
444
|
+
after = pageInfo?.hasNextPage ? pageInfo.endCursor : null;
|
|
445
|
+
} while (after);
|
|
446
|
+
return locales;
|
|
447
|
+
}
|
|
371
448
|
pushLocale(locale) {
|
|
372
449
|
return this.client
|
|
373
450
|
.request((0, graphql_request_1.gql) `
|
|
@@ -417,12 +494,14 @@ class FcodeGraphQLClient {
|
|
|
417
494
|
mutation deleteLocale($locale: String!) {
|
|
418
495
|
deleteLocale(locale: $locale) {
|
|
419
496
|
id
|
|
497
|
+
deletedAt
|
|
498
|
+
retainingVersionTags
|
|
420
499
|
}
|
|
421
500
|
}
|
|
422
501
|
`, {
|
|
423
502
|
locale,
|
|
424
503
|
})
|
|
425
|
-
.then(() =>
|
|
504
|
+
.then((data) => data.deleteLocale)
|
|
426
505
|
.catch((error) => {
|
|
427
506
|
return Promise.reject(this.cleanError(error));
|
|
428
507
|
});
|
|
@@ -431,16 +510,30 @@ class FcodeGraphQLClient {
|
|
|
431
510
|
const settings = {
|
|
432
511
|
dependencies: { scopedToProcess: process.settings.dependencies.scopedToProcess },
|
|
433
512
|
};
|
|
513
|
+
// The platform mirrors `factorial` into the legacy `formSettings` / `uiTrigger` blocks, so
|
|
514
|
+
// once metadata.json carries it the legacy keys only contribute what the mirror leaves alone
|
|
515
|
+
// (the form's auth mode, and every app role but `USER_FACING_FORM`, which the block owns as
|
|
516
|
+
// `form.appTool`); their enablement is not sent, or the last one applied would win over the
|
|
517
|
+
// block the developer actually edits.
|
|
518
|
+
if (process.factorial) {
|
|
519
|
+
settings.factorial = this.toFactorialInput(process.factorial, process.form);
|
|
520
|
+
}
|
|
434
521
|
if (process.form) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
522
|
+
const formSettings = process.factorial ? {} : { enabled: process.form.enabled };
|
|
523
|
+
// `USER_FACING_FORM` is the one app role the `factorial` block owns, as `form.appTool`, so
|
|
524
|
+
// once the block is there that value travels with it; the lifecycle roles, which the block
|
|
525
|
+
// has no switch for, keep coming from here.
|
|
526
|
+
if (process.form.appRole && !(process.factorial && process.form.appRole === 'USER_FACING_FORM')) {
|
|
527
|
+
formSettings.appRole = process.form.appRole;
|
|
438
528
|
}
|
|
439
529
|
if (process.form.authMode) {
|
|
440
|
-
|
|
530
|
+
formSettings.authMode = process.form.authMode;
|
|
531
|
+
}
|
|
532
|
+
if (Object.keys(formSettings).length > 0) {
|
|
533
|
+
settings.formSettings = formSettings;
|
|
441
534
|
}
|
|
442
535
|
}
|
|
443
|
-
if (process.uiTrigger) {
|
|
536
|
+
if (process.uiTrigger && !process.factorial) {
|
|
444
537
|
settings.uiTrigger = { enabled: process.uiTrigger.enabled };
|
|
445
538
|
if (process.uiTrigger.locationId) {
|
|
446
539
|
settings.uiTrigger.locationId = process.uiTrigger.locationId;
|
|
@@ -531,11 +624,14 @@ class FcodeGraphQLClient {
|
|
|
531
624
|
mutation deleteProcess($id: ID!) {
|
|
532
625
|
deleteProcess(id: $id) {
|
|
533
626
|
id
|
|
627
|
+
deletedAt
|
|
628
|
+
retainingVersionTags
|
|
534
629
|
}
|
|
535
630
|
}
|
|
536
631
|
`, {
|
|
537
632
|
id,
|
|
538
633
|
})
|
|
634
|
+
.then((data) => data.deleteProcess)
|
|
539
635
|
.catch((error) => {
|
|
540
636
|
return Promise.reject(this.cleanError(error));
|
|
541
637
|
});
|
|
@@ -590,11 +686,14 @@ class FcodeGraphQLClient {
|
|
|
590
686
|
mutation deleteScriptLibrary($id: ID!) {
|
|
591
687
|
deleteScriptLibrary(id: $id) {
|
|
592
688
|
id
|
|
689
|
+
deletedAt
|
|
690
|
+
retainingVersionTags
|
|
593
691
|
}
|
|
594
692
|
}
|
|
595
693
|
`, {
|
|
596
694
|
id,
|
|
597
695
|
})
|
|
696
|
+
.then((data) => data.deleteScriptLibrary)
|
|
598
697
|
.catch((error) => {
|
|
599
698
|
return Promise.reject(this.cleanError(error));
|
|
600
699
|
});
|
|
@@ -725,6 +824,12 @@ class FcodeGraphQLClient {
|
|
|
725
824
|
inheritedTeam(resolvingTeamSlug) {
|
|
726
825
|
return resolvingTeamSlug && resolvingTeamSlug !== this.teamSlug ? resolvingTeamSlug : undefined;
|
|
727
826
|
}
|
|
827
|
+
deletionOf(deletedAt, retainingVersionTags) {
|
|
828
|
+
return deletedAt ? { deletedAt, retainingVersionTags: retainingVersionTags ?? [] } : undefined;
|
|
829
|
+
}
|
|
830
|
+
isUndefinedDeletionFieldError(error) {
|
|
831
|
+
return DELETION_QUERY_FIELDS.some((field) => this.isUndefinedFieldError(error, field));
|
|
832
|
+
}
|
|
728
833
|
/**
|
|
729
834
|
* The public mode and the default header name are left out, so a public webhook — or one
|
|
730
835
|
* using the default header — keeps the metadata.json it already had on disk.
|
|
@@ -747,7 +852,112 @@ class FcodeGraphQLClient {
|
|
|
747
852
|
}
|
|
748
853
|
return webhook;
|
|
749
854
|
}
|
|
750
|
-
|
|
855
|
+
/**
|
|
856
|
+
* metadata.json omits every defaulted field of the `factorial` block, so the input restores
|
|
857
|
+
* them explicitly: every input field left out means "leave unchanged" on the platform, and the
|
|
858
|
+
* file is the source of truth. That includes the agent tool's clearable texts, sent as "" when
|
|
859
|
+
* the file has none so deleting the line clears the value. The platform reads a null or empty
|
|
860
|
+
* `uiTrigger.locationId` / `label` / `icon` and `agentTool.effect` as unchanged, though, so
|
|
861
|
+
* those are sent only when present (like the legacy `uiTrigger.label`).
|
|
862
|
+
*/
|
|
863
|
+
toFactorialInput(factorial, form) {
|
|
864
|
+
const input = {
|
|
865
|
+
enabled: factorial.enabled,
|
|
866
|
+
awaitResult: factorial.awaitResult ?? true,
|
|
867
|
+
requiredPolicies: factorial.requiredPolicies ?? [],
|
|
868
|
+
uiTrigger: { enabled: Boolean(factorial.uiTrigger?.enabled) },
|
|
869
|
+
form: {
|
|
870
|
+
enabled: Boolean(factorial.form?.enabled),
|
|
871
|
+
public: Boolean(factorial.form?.public),
|
|
872
|
+
// A file written before the flag existed says nothing about `appTool`, and the block wins
|
|
873
|
+
// over the legacy `appRole` applied before it — so an absent flag falls back to the role
|
|
874
|
+
// rather than to `false`, which would release it.
|
|
875
|
+
appTool: factorial.form?.appTool ?? form?.appRole === 'USER_FACING_FORM',
|
|
876
|
+
},
|
|
877
|
+
agentTool: {
|
|
878
|
+
enabled: Boolean(factorial.agentTool?.enabled),
|
|
879
|
+
whenNotToUse: factorial.agentTool?.whenNotToUse ?? [],
|
|
880
|
+
preconditions: factorial.agentTool?.preconditions ?? [],
|
|
881
|
+
doesNotDo: factorial.agentTool?.doesNotDo ?? [],
|
|
882
|
+
},
|
|
883
|
+
backend: { enabled: Boolean(factorial.backend?.enabled) },
|
|
884
|
+
};
|
|
885
|
+
for (const field of AGENT_TOOL_CLEARABLE_TEXT_FIELDS) {
|
|
886
|
+
input.agentTool[field] = factorial.agentTool?.[field] ?? '';
|
|
887
|
+
}
|
|
888
|
+
if (factorial.agentTool?.effect) {
|
|
889
|
+
input.agentTool.effect = factorial.agentTool.effect;
|
|
890
|
+
}
|
|
891
|
+
if (factorial.uiTrigger?.locationId) {
|
|
892
|
+
input.uiTrigger.locationId = factorial.uiTrigger.locationId;
|
|
893
|
+
}
|
|
894
|
+
if (factorial.uiTrigger?.label) {
|
|
895
|
+
input.uiTrigger.label = factorial.uiTrigger.label;
|
|
896
|
+
}
|
|
897
|
+
if (factorial.uiTrigger?.icon) {
|
|
898
|
+
input.uiTrigger.icon = factorial.uiTrigger.icon;
|
|
899
|
+
}
|
|
900
|
+
return input;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* The `factorial` block as metadata.json writes it: defaulted fields omitted, and a block or
|
|
904
|
+
* sub-block omitted only when disabled AND all defaults — a disabled sub-block that still carries
|
|
905
|
+
* settings (a tool contract, a trigger location, a public form) is kept, or a pull followed by a
|
|
906
|
+
* push would clear them on the platform. The platform always returns the block, so a process
|
|
907
|
+
* that never configured one comes back as all defaults and is mapped to no block at all, or every
|
|
908
|
+
* process in every workspace would gain a `factorial` key (and a new content hash) on its next
|
|
909
|
+
* pull.
|
|
910
|
+
*/
|
|
911
|
+
mapFactorialSettings(factorial) {
|
|
912
|
+
if (!factorial) {
|
|
913
|
+
return undefined;
|
|
914
|
+
}
|
|
915
|
+
const mapped = { enabled: Boolean(factorial.enabled) };
|
|
916
|
+
if (factorial.awaitResult === false) {
|
|
917
|
+
mapped.awaitResult = false;
|
|
918
|
+
}
|
|
919
|
+
if (Array.isArray(factorial.requiredPolicies) && factorial.requiredPolicies.length > 0) {
|
|
920
|
+
mapped.requiredPolicies = factorial.requiredPolicies;
|
|
921
|
+
}
|
|
922
|
+
if (factorial.uiTrigger) {
|
|
923
|
+
const uiTrigger = { enabled: Boolean(factorial.uiTrigger.enabled) };
|
|
924
|
+
for (const field of ['locationId', 'label', 'icon']) {
|
|
925
|
+
if (factorial.uiTrigger[field]) {
|
|
926
|
+
uiTrigger[field] = factorial.uiTrigger[field];
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
mapped.uiTrigger = unlessAllDefaults(uiTrigger);
|
|
930
|
+
}
|
|
931
|
+
if (factorial.form) {
|
|
932
|
+
const form = { enabled: Boolean(factorial.form.enabled) };
|
|
933
|
+
if (factorial.form.public) {
|
|
934
|
+
form.public = true;
|
|
935
|
+
}
|
|
936
|
+
if (factorial.form.appTool) {
|
|
937
|
+
form.appTool = true;
|
|
938
|
+
}
|
|
939
|
+
mapped.form = unlessAllDefaults(form);
|
|
940
|
+
}
|
|
941
|
+
if (factorial.agentTool) {
|
|
942
|
+
const agentTool = { enabled: Boolean(factorial.agentTool.enabled) };
|
|
943
|
+
for (const field of AGENT_TOOL_TEXT_FIELDS) {
|
|
944
|
+
if (factorial.agentTool[field]) {
|
|
945
|
+
agentTool[field] = factorial.agentTool[field];
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const field of AGENT_TOOL_LIST_FIELDS) {
|
|
949
|
+
if (Array.isArray(factorial.agentTool[field]) && factorial.agentTool[field].length > 0) {
|
|
950
|
+
agentTool[field] = factorial.agentTool[field];
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
mapped.agentTool = unlessAllDefaults(agentTool);
|
|
954
|
+
}
|
|
955
|
+
if (factorial.backend?.enabled) {
|
|
956
|
+
mapped.backend = { enabled: true };
|
|
957
|
+
}
|
|
958
|
+
return unlessAllDefaults(_.omitBy(mapped, _.isUndefined));
|
|
959
|
+
}
|
|
960
|
+
mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug, lifecycleRole, deletedAt, retainingVersionTags, }) {
|
|
751
961
|
const dependencies = _.map(manifest?.dependencies, (dependency) => {
|
|
752
962
|
return { name: dependency.name, specifier: dependency.gitUrl ?? dependency.semVer };
|
|
753
963
|
});
|
|
@@ -783,7 +993,15 @@ class FcodeGraphQLClient {
|
|
|
783
993
|
metadata.uiTrigger.awaitResult = true;
|
|
784
994
|
}
|
|
785
995
|
}
|
|
786
|
-
|
|
996
|
+
const factorial = this.mapFactorialSettings(settings?.factorial);
|
|
997
|
+
if (factorial) {
|
|
998
|
+
metadata.factorial = factorial;
|
|
999
|
+
}
|
|
1000
|
+
const process = new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug), this.deletionOf(deletedAt, retainingVersionTags));
|
|
1001
|
+
if (lifecycleRole) {
|
|
1002
|
+
process.lifecycleRole = lifecycleRole;
|
|
1003
|
+
}
|
|
1004
|
+
return process;
|
|
787
1005
|
}
|
|
788
1006
|
/**
|
|
789
1007
|
* 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?).");
|
|
@@ -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>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commandLineFor = commandLineFor;
|
|
4
|
+
exports.spawnCommand = spawnCommand;
|
|
5
|
+
exports.binShimPath = binShimPath;
|
|
6
|
+
exports.venvExecutablePath = venvExecutablePath;
|
|
7
|
+
exports.venvPythonPath = venvPythonPath;
|
|
8
|
+
exports.childEnv = childEnv;
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const quoteForCmd = (part) => (/[\s"]/.test(part) ? `"${part.replace(/"/g, '""')}"` : part);
|
|
12
|
+
/**
|
|
13
|
+
* How to invoke a package-manager executable (`npm`, `npx`, a `node_modules/.bin` shim) on `platform`.
|
|
14
|
+
*
|
|
15
|
+
* On Windows those are `.cmd` batch files, which Node refuses to spawn without a shell, so the
|
|
16
|
+
* command line goes through `cmd.exe` with every part quoted for it.
|
|
17
|
+
*/
|
|
18
|
+
function commandLineFor(command, args, platform = process.platform) {
|
|
19
|
+
if (platform !== 'win32') {
|
|
20
|
+
return { command, args, shell: false };
|
|
21
|
+
}
|
|
22
|
+
return { command: quoteForCmd(command), args: args.map(quoteForCmd), shell: true };
|
|
23
|
+
}
|
|
24
|
+
function spawnCommand(command, args, options = {}) {
|
|
25
|
+
const line = commandLineFor(command, args);
|
|
26
|
+
return (0, node_child_process_1.spawn)(line.command, line.args, { ...options, shell: line.shell });
|
|
27
|
+
}
|
|
28
|
+
/** The `node_modules/.bin` shim `name` resolves to on `platform`. */
|
|
29
|
+
function binShimPath(nodeModulesPath, name, platform = process.platform) {
|
|
30
|
+
return path.join(nodeModulesPath, '.bin', platform === 'win32' ? `${name}.cmd` : name);
|
|
31
|
+
}
|
|
32
|
+
/** The executable `name` a Python virtual environment at `envPath` exposes on `platform`. */
|
|
33
|
+
function venvExecutablePath(envPath, name, platform = process.platform) {
|
|
34
|
+
return platform === 'win32' ? path.join(envPath, 'Scripts', `${name}.exe`) : path.join(envPath, 'bin', name);
|
|
35
|
+
}
|
|
36
|
+
/** The Python interpreter of the virtual environment at `envPath`; Windows venvs ship no `python3` alias. */
|
|
37
|
+
function venvPythonPath(envPath, platform = process.platform) {
|
|
38
|
+
return venvExecutablePath(envPath, platform === 'win32' ? 'python' : 'python3', platform);
|
|
39
|
+
}
|
|
40
|
+
const WINDOWS_SYSTEM_ENV_KEYS = ['SYSTEMROOT', 'SYSTEMDRIVE', 'WINDIR', 'COMSPEC', 'PATHEXT', 'TEMP', 'TMP', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA'];
|
|
41
|
+
/**
|
|
42
|
+
* An environment for a process run locally: the entries given plus, on Windows, the system
|
|
43
|
+
* variables Node and Python cannot start without (`SYSTEMROOT`, `TEMP`, ...).
|
|
44
|
+
*/
|
|
45
|
+
function childEnv(entries, platform = process.platform, hostEnv = process.env) {
|
|
46
|
+
const env = {};
|
|
47
|
+
if (platform === 'win32') {
|
|
48
|
+
for (const [key, value] of Object.entries(hostEnv)) {
|
|
49
|
+
if (value !== undefined && WINDOWS_SYSTEM_ENV_KEYS.includes(key.toUpperCase())) {
|
|
50
|
+
env[key] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
55
|
+
if (value !== undefined) {
|
|
56
|
+
env[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return env;
|
|
60
|
+
}
|
package/oclif.manifest.json
CHANGED