@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.
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 +76 -3
  19. package/dist/core/types.js +33 -6
  20. package/dist/repositories/locale.js +5 -3
  21. package/dist/repositories/process.js +20 -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 +45 -7
  25. package/dist/services/fcode-graphql-client.js +264 -46
  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
@@ -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
- allProcesses() {
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
- allScriptLibraries() {
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
- const pageSize = 200;
330
- const locales = [];
331
- let after = null;
394
+ async allLocales(includeDeleted = true) {
395
+ let locales;
332
396
  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);
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(() => undefined)
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
- settings.formSettings = { enabled: process.form.enabled };
436
- if (process.form.appRole) {
437
- settings.formSettings.appRole = process.form.appRole;
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
- settings.formSettings.authMode = process.form.authMode;
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
- mapProcess({ id, slug, name, description, tags, script, readme, manifest, settings, triggers, resolvingTeamSlug }) {
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
- return new types_1.Process(id, slug, script, readme, { dependencies }, settings, metadata, this.inheritedTeam(resolvingTeamSlug));
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, 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>;
@@ -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
+ }
@@ -2516,5 +2516,5 @@
2516
2516
  ]
2517
2517
  }
2518
2518
  },
2519
- "version": "3.2.0"
2519
+ "version": "3.4.0"
2520
2520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factorialco/fcode-cli",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "packageManager": "pnpm@11.7.0",
5
5
  "description": "Factorial Code Command Line Interface",
6
6
  "homepage": "https://code.factorialhr.com/docs/cli/",