@factorialco/fcode-cli 2.3.0 → 3.0.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 (123) hide show
  1. package/README.md +334 -117
  2. package/dist/commands/clone.d.ts +0 -12
  3. package/dist/commands/clone.js +22 -135
  4. package/dist/commands/{settings → config}/get.d.ts +1 -1
  5. package/dist/commands/{settings → config}/get.js +7 -7
  6. package/dist/commands/{settings → config}/remove.d.ts +1 -1
  7. package/dist/commands/{settings → config}/remove.js +6 -6
  8. package/dist/commands/{settings → config}/set.d.ts +1 -1
  9. package/dist/commands/{settings → config}/set.js +8 -8
  10. package/dist/commands/dependencies/diff.d.ts +9 -0
  11. package/dist/commands/dependencies/diff.js +15 -0
  12. package/dist/commands/dependencies/pull.d.ts +9 -0
  13. package/dist/commands/dependencies/pull.js +44 -1
  14. package/dist/commands/dependencies/status.d.ts +11 -0
  15. package/dist/commands/dependencies/status.js +43 -0
  16. package/dist/commands/diff.d.ts +12 -0
  17. package/dist/commands/diff.js +48 -0
  18. package/dist/commands/http.d.ts +1 -1
  19. package/dist/commands/http.js +7 -7
  20. package/dist/commands/i18n/diff.d.ts +9 -0
  21. package/dist/commands/i18n/diff.js +15 -0
  22. package/dist/commands/i18n/status.d.ts +3 -0
  23. package/dist/commands/i18n/status.js +1 -0
  24. package/dist/commands/modules/diff.d.ts +9 -0
  25. package/dist/commands/modules/diff.js +15 -0
  26. package/dist/commands/modules/status.d.ts +3 -0
  27. package/dist/commands/modules/status.js +1 -0
  28. package/dist/commands/processes/diff.d.ts +9 -0
  29. package/dist/commands/processes/diff.js +15 -0
  30. package/dist/commands/processes/status.d.ts +3 -0
  31. package/dist/commands/processes/status.js +1 -0
  32. package/dist/commands/pull.js +3 -3
  33. package/dist/commands/push.js +2 -2
  34. package/dist/commands/{team → settings}/aliases/delete.d.ts +1 -1
  35. package/dist/commands/{team → settings}/aliases/delete.js +6 -6
  36. package/dist/commands/{team → settings}/aliases/list.d.ts +1 -1
  37. package/dist/commands/{team → settings}/aliases/list.js +8 -8
  38. package/dist/commands/{team → settings}/aliases/set.d.ts +1 -1
  39. package/dist/commands/{team → settings}/aliases/set.js +8 -8
  40. package/dist/commands/settings/diff.d.ts +8 -0
  41. package/dist/commands/settings/diff.js +39 -0
  42. package/dist/commands/{team/push.d.ts → settings/pull.d.ts} +3 -3
  43. package/dist/commands/settings/pull.js +41 -0
  44. package/dist/commands/settings/push.d.ts +13 -0
  45. package/dist/commands/settings/push.js +103 -0
  46. package/dist/commands/settings/status.d.ts +8 -0
  47. package/dist/commands/settings/status.js +46 -0
  48. package/dist/commands/{team → settings}/versions/create.d.ts +1 -2
  49. package/dist/commands/settings/versions/create.js +36 -0
  50. package/dist/commands/{team → settings}/versions/delete.d.ts +1 -1
  51. package/dist/commands/{team → settings}/versions/delete.js +10 -10
  52. package/dist/commands/{team → settings}/versions/list.d.ts +1 -1
  53. package/dist/commands/{team → settings}/versions/list.js +8 -8
  54. package/dist/commands/status.d.ts +3 -0
  55. package/dist/commands/status.js +10 -7
  56. package/dist/commands/team/clone.d.ts +14 -0
  57. package/dist/commands/team/clone.js +91 -0
  58. package/dist/commands/team/pull.d.ts +3 -4
  59. package/dist/commands/team/pull.js +25 -19
  60. package/dist/commands/team/status.d.ts +3 -4
  61. package/dist/commands/team/status.js +34 -27
  62. package/dist/commands/variables/diff.d.ts +9 -0
  63. package/dist/commands/variables/diff.js +15 -0
  64. package/dist/commands/variables/status.d.ts +3 -0
  65. package/dist/commands/variables/status.js +1 -0
  66. package/dist/config/dev-team.d.ts +48 -0
  67. package/dist/config/dev-team.js +62 -0
  68. package/dist/config/workspace.d.ts +2 -2
  69. package/dist/config/workspace.js +4 -2
  70. package/dist/core/commands/base-command.js +5 -0
  71. package/dist/core/commands/dependency-command.d.ts +2 -1
  72. package/dist/core/commands/dependency-command.js +3 -0
  73. package/dist/core/commands/i18n-command.js +2 -0
  74. package/dist/core/commands/modules-command.js +2 -0
  75. package/dist/core/commands/processes-command.js +2 -0
  76. package/dist/core/commands/resource-command.d.ts +26 -1
  77. package/dist/core/commands/resource-command.js +100 -3
  78. package/dist/core/commands/variables-command.d.ts +7 -0
  79. package/dist/core/commands/variables-command.js +13 -0
  80. package/dist/core/dependencies/env-dependencies-command.js +6 -3
  81. package/dist/core/executors/javascript-execution-task.js +36 -5
  82. package/dist/core/types.d.ts +59 -9
  83. package/dist/core/types.js +64 -10
  84. package/dist/core/utils/dependencies.js +4 -1
  85. package/dist/env.js +1 -1
  86. package/dist/repositories/process.d.ts +8 -2
  87. package/dist/repositories/process.js +29 -15
  88. package/dist/repositories/programming-language-manifest.d.ts +31 -1
  89. package/dist/repositories/programming-language-manifest.js +96 -2
  90. package/dist/repositories/settings.d.ts +52 -0
  91. package/dist/repositories/settings.js +214 -0
  92. package/dist/runtime/libs/python/fcode/context.py +42 -8
  93. package/dist/services/dashboard-client.d.ts +31 -0
  94. package/dist/services/dashboard-client.js +99 -0
  95. package/dist/services/dev-team-service.d.ts +41 -0
  96. package/dist/services/dev-team-service.js +162 -0
  97. package/dist/services/fcode-graphql-client.d.ts +17 -2
  98. package/dist/services/fcode-graphql-client.js +127 -15
  99. package/dist/services/locale-resolver.d.ts +17 -1
  100. package/dist/services/locale-resolver.js +62 -7
  101. package/dist/services/resource-differ.d.ts +60 -0
  102. package/dist/services/resource-differ.js +135 -0
  103. package/dist/services/skills-installer.d.ts +16 -0
  104. package/dist/services/skills-installer.js +77 -0
  105. package/dist/services/team-versioning.d.ts +16 -0
  106. package/dist/services/team-versioning.js +40 -0
  107. package/dist/services/webhook-auth.js +1 -1
  108. package/dist/services/workspace-cloner.d.ts +31 -0
  109. package/dist/services/workspace-cloner.js +95 -0
  110. package/dist/utils/dev-slug.d.ts +12 -0
  111. package/dist/utils/dev-slug.js +37 -0
  112. package/dist/utils/diff.d.ts +19 -0
  113. package/dist/utils/diff.js +58 -0
  114. package/dist/utils/gitignore.d.ts +3 -1
  115. package/dist/utils/gitignore.js +11 -2
  116. package/dist/utils/slugify.d.ts +17 -0
  117. package/dist/utils/slugify.js +41 -0
  118. package/oclif.manifest.json +433 -113
  119. package/package.json +12 -7
  120. package/dist/commands/team/push.js +0 -52
  121. package/dist/commands/team/versions/create.js +0 -42
  122. package/dist/repositories/team.d.ts +0 -31
  123. package/dist/repositories/team.js +0 -127
@@ -4,6 +4,7 @@ const script_library_1 = require("../../repositories/script-library");
4
4
  const resource_merger_1 = require("../resource-merger");
5
5
  const resource_pusher_1 = require("../resource-pusher");
6
6
  const resource_command_1 = require("./resource-command");
7
+ const resource_differ_1 = require("../../services/resource-differ");
7
8
  class ModulesCommand extends resource_command_1.default {
8
9
  async init() {
9
10
  await super.init();
@@ -11,6 +12,7 @@ class ModulesCommand extends resource_command_1.default {
11
12
  this._resourceMerger = new resource_merger_1.default(this._resourceRepository);
12
13
  this._resourcePusher = new resource_pusher_1.default(this._resourceRepository, this._fcodeGraphQLClient);
13
14
  this._resourceQuery = () => this._fcodeGraphQLClient.allScriptLibraries.bind(this._fcodeGraphQLClient)();
15
+ this._diffFilesFor = resource_differ_1.moduleDiffFiles;
14
16
  this._versionedResourceQuery = (id) => this._fcodeGraphQLClient.allVersionedScriptLibraries.bind(this._fcodeGraphQLClient)(id);
15
17
  this._pushVersionedResourceMutation = (versionedScriptLibrary) => this._fcodeGraphQLClient.createVersionedScriptLibrary.bind(this._fcodeGraphQLClient)(versionedScriptLibrary);
16
18
  }
@@ -5,6 +5,7 @@ const process_1 = require("../../repositories/process");
5
5
  const resource_merger_1 = require("../resource-merger");
6
6
  const resource_pusher_1 = require("../resource-pusher");
7
7
  const resource_command_1 = require("./resource-command");
8
+ const resource_differ_1 = require("../../services/resource-differ");
8
9
  class ProcessesCommand extends resource_command_1.default {
9
10
  async init() {
10
11
  await super.init();
@@ -12,6 +13,7 @@ class ProcessesCommand extends resource_command_1.default {
12
13
  this._resourceMerger = new resource_merger_1.default(this._resourceRepository);
13
14
  this._resourcePusher = new resource_pusher_1.default(this._resourceRepository, this._fcodeGraphQLClient);
14
15
  this._resourceQuery = () => this._fcodeGraphQLClient.allProcesses.bind(this._fcodeGraphQLClient)();
16
+ this._diffFilesFor = resource_differ_1.processDiffFiles;
15
17
  this._versionedResourceQuery = (id) => this._fcodeGraphQLClient.allVersionedProcesses.bind(this._fcodeGraphQLClient)(id);
16
18
  this._pushVersionedResourceMutation = (versionedProcess) => this._fcodeGraphQLClient.createVersionedProcesses.bind(this._fcodeGraphQLClient)(versionedProcess);
17
19
  }
@@ -1,12 +1,22 @@
1
- import type { AddResourceOptions, MergedResource, Resource } from '../types';
1
+ import { ResourceStatus, type AddResourceOptions, type MergedResource, type Resource } from '../types';
2
2
  import ResourceRepositoryBase from '../../repositories/resource';
3
3
  import ResourceMerger from '../resource-merger';
4
4
  import ResourcePusher from '../resource-pusher';
5
5
  import ApiClientCommand from './api-client-command';
6
+ import { PseudoFile } from '../../utils/diff';
7
+ import { DiffSection, ResourcePair } from '../../services/resource-differ';
8
+ /** A status meaning the two sides agree, so there is nothing to pull, push or diff. */
9
+ export declare const isUnchangedStatus: (status: ResourceStatus) => boolean;
10
+ /**
11
+ * An inherited resource the parent workspace owns and the cloud agrees on is noise in `status`:
12
+ * there is nothing to pull or push. It comes back as soon as it differs — or with --showInherited.
13
+ */
14
+ export declare const isUnchangedInherited: (resource: MergedResource) => boolean;
6
15
  export default abstract class ResourceCommand<R extends Resource> extends ApiClientCommand {
7
16
  _resourceMerger: ResourceMerger<R>;
8
17
  _resourcePusher: ResourcePusher<R>;
9
18
  _resourceQuery: () => Promise<Array<R>>;
19
+ _diffFilesFor: (pair: ResourcePair<R>) => Array<PseudoFile>;
10
20
  _resourceRepository: ResourceRepositoryBase<R>;
11
21
  protected resourceSlug: string;
12
22
  protected filterResource: (slug: string) => boolean;
@@ -20,5 +30,20 @@ export default abstract class ResourceCommand<R extends Resource> extends ApiCli
20
30
  removeResource(slug: string): Promise<void>;
21
31
  pushResources(): Promise<void>;
22
32
  resourcesLog(): Promise<void>;
33
+ /**
34
+ * One heading per resource, so a patch is always attributable to a slug and its status. Variables
35
+ * override this: they share a single .env file, and one patch over it reads better than one per key.
36
+ *
37
+ * `allPairs` carries every resource, including the unchanged ones, for a renderer that needs the
38
+ * whole set to place a change in context; `diffable` is only what a status says has changed.
39
+ */
40
+ protected diffSections(diffable: Array<ResourcePair<R>>, allPairs: Array<ResourcePair<R>>): Array<DiffSection>;
41
+ /** Names the resource a patch belongs to, flagging an inherited one as the parent's to fix. */
42
+ protected diffLabel(pair: ResourcePair<R>): string;
43
+ /**
44
+ * Prints a unified diff of everything that differs between the workspace and the cloud, and
45
+ * reports whether anything did — which is what makes `fcode diff` exit non-zero.
46
+ */
47
+ resourcesDiff(): Promise<boolean>;
23
48
  resourcesTableLog(resources: Array<MergedResource>): Promise<void>;
24
49
  }
@@ -1,9 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isUnchangedInherited = exports.isUnchangedStatus = void 0;
3
4
  const color_1 = require("@oclif/color");
4
5
  const cli_1 = require("../../utils/cli");
6
+ const types_1 = require("../types");
5
7
  const api_client_command_1 = require("./api-client-command");
6
8
  const colorize_resource_1 = require("../../utils/colorize-resource");
9
+ const diff_1 = require("../../utils/diff");
10
+ const resource_differ_1 = require("../../services/resource-differ");
11
+ // A sensitive variable never compares equal — its content hash is empty — so SENSITIVE is the
12
+ // resting state of an inherited secret, not a change, and counts as unchanged here.
13
+ const UNCHANGED_STATUSES = new Set([types_1.ResourceStatus.UP_TO_DATE, types_1.ResourceStatus.SENSITIVE]);
14
+ /** A status meaning the two sides agree, so there is nothing to pull, push or diff. */
15
+ const isUnchangedStatus = (status) => UNCHANGED_STATUSES.has(status);
16
+ exports.isUnchangedStatus = isUnchangedStatus;
17
+ /**
18
+ * An inherited resource the parent workspace owns and the cloud agrees on is noise in `status`:
19
+ * there is nothing to pull or push. It comes back as soon as it differs — or with --showInherited.
20
+ */
21
+ const isUnchangedInherited = (resource) => Boolean(resource.resolvingTeamSlug) && (0, exports.isUnchangedStatus)(resource.status);
22
+ exports.isUnchangedInherited = isUnchangedInherited;
7
23
  class ResourceCommand extends api_client_command_1.default {
8
24
  constructor() {
9
25
  super(...arguments);
@@ -60,18 +76,99 @@ class ResourceCommand extends api_client_command_1.default {
60
76
  async resourcesLog() {
61
77
  cli_1.default.log(color_1.default.cyan(`\n${this._resourceRepository._repositoryName} status`));
62
78
  const mergedResources = await this._resourceMerger.mergeStatus(await this.getRemoteResources(), await this.getLocalResources());
63
- if (mergedResources.length > 0) {
64
- await this.resourcesTableLog(mergedResources);
79
+ const { flags: { showInherited }, } = await this.parse(this.ctor);
80
+ const visibleResources = showInherited ? mergedResources : mergedResources.filter((resource) => !(0, exports.isUnchangedInherited)(resource));
81
+ const hiddenCount = mergedResources.length - visibleResources.length;
82
+ if (visibleResources.length > 0) {
83
+ await this.resourcesTableLog(visibleResources);
65
84
  }
66
- else {
85
+ else if (mergedResources.length === 0) {
67
86
  cli_1.default.log(`No ${this._resourceRepository._repositoryName} found`);
68
87
  }
88
+ if (hiddenCount > 0) {
89
+ cli_1.default.log(color_1.default.cyan(`ℹ️ ${hiddenCount} inherited ${this._resourceRepository._repositoryName} up to date (hidden). Use --showInherited to list them.`));
90
+ }
69
91
  const slugsNotAdded = await this.findSlugsNotAdded();
70
92
  if (slugsNotAdded.length > 0) {
71
93
  const warningText = `\nℹ️ ${slugsNotAdded.length} ${this._resourceRepository._repositoryName} only existing in local: ${slugsNotAdded.join(', ')}\n Use command 'fcode ${this._resourceRepository._repositoryName}:add' to keep track of these resources`;
72
94
  cli_1.default.log(color_1.default.yellow(warningText));
73
95
  }
74
96
  }
97
+ /**
98
+ * One heading per resource, so a patch is always attributable to a slug and its status. Variables
99
+ * override this: they share a single .env file, and one patch over it reads better than one per key.
100
+ *
101
+ * `allPairs` carries every resource, including the unchanged ones, for a renderer that needs the
102
+ * whole set to place a change in context; `diffable` is only what a status says has changed.
103
+ */
104
+ diffSections(diffable, allPairs) {
105
+ return diffable.map((pair) => ({
106
+ label: this.diffLabel(pair),
107
+ files: this._diffFilesFor(pair),
108
+ }));
109
+ }
110
+ /** Names the resource a patch belongs to, flagging an inherited one as the parent's to fix. */
111
+ diffLabel(pair) {
112
+ const label = `${pair.status} ${this._resourceRepository._repositoryName}/${pair.slug}`;
113
+ const inheritedTeam = (0, resource_differ_1.inheritedFrom)(pair);
114
+ return inheritedTeam ? `${label} 🔗 inherited from '${inheritedTeam}' (read-only — run pull to update)` : label;
115
+ }
116
+ /**
117
+ * Prints a unified diff of everything that differs between the workspace and the cloud, and
118
+ * reports whether anything did — which is what makes `fcode diff` exit non-zero.
119
+ */
120
+ async resourcesDiff() {
121
+ cli_1.default.log(color_1.default.cyan(`\n${this._resourceRepository._repositoryName} diff`));
122
+ const pairs = await (0, resource_differ_1.pairResources)(await this.getRemoteResources(), await this.getLocalResources(), (slug) => this._resourceRepository.existsMetadata(slug));
123
+ const notes = [];
124
+ const diffable = [];
125
+ let hiddenInherited = 0;
126
+ let sensitivityChanged = false;
127
+ for (const pair of pairs) {
128
+ // Same rule `status` applies: an inherited resource the cloud agrees on has nothing to show,
129
+ // but one that drifted is exactly what the user needs — a parent moving it is never swallowed.
130
+ if ((0, resource_differ_1.inheritedFrom)(pair) && (0, exports.isUnchangedStatus)(pair.status)) {
131
+ hiddenInherited++;
132
+ continue;
133
+ }
134
+ // A sensitive value is unknowable on both sides, so there is nothing to diff; a sensitivity
135
+ // change is a real difference the patch cannot show, so it is reported instead.
136
+ if (pair.status === types_1.ResourceStatus.SENSITIVITY_CHANGED) {
137
+ sensitivityChanged = true;
138
+ notes.push(color_1.default.yellow(`${pair.status} ${pair.slug} — sensitivity differs between local and cloud (values are never shown)`));
139
+ continue;
140
+ }
141
+ // An up-to-date resource is skipped rather than rendered and found equal: its two sides can
142
+ // still serialize differently (key order in parametersSchema.json, say) where the content
143
+ // hash — which is what "up to date" means — says they are the same.
144
+ if ((0, exports.isUnchangedStatus)(pair.status)) {
145
+ continue;
146
+ }
147
+ diffable.push(pair);
148
+ }
149
+ const sections = this.diffSections(diffable, pairs)
150
+ .map((section) => ({ ...section, files: section.files.filter((file) => (0, diff_1.hasDifference)(file)) }))
151
+ .filter((section) => section.files.length > 0);
152
+ for (const section of sections) {
153
+ cli_1.default.log('');
154
+ cli_1.default.log((0, colorize_resource_1.colorizeByStatus)(section.label, section.label));
155
+ for (const file of section.files) {
156
+ cli_1.default.log((0, diff_1.colorizeDiff)((0, diff_1.renderUnifiedDiff)(file)));
157
+ }
158
+ }
159
+ for (const note of notes) {
160
+ cli_1.default.log(note);
161
+ }
162
+ if (sections.length === 0 && !sensitivityChanged) {
163
+ cli_1.default.log(color_1.default.green(`✅ ${this._resourceRepository._repositoryName} up to date`));
164
+ }
165
+ // Counted rather than listed: a workspace built on a parent inherits dozens of resources, and a
166
+ // line each would bury the diff it was run for. Only the ones with nothing to show are in here.
167
+ if (hiddenInherited > 0) {
168
+ cli_1.default.log(color_1.default.cyan(`ℹ️ ${hiddenInherited} inherited ${this._resourceRepository._repositoryName} up to date (nothing to diff).`));
169
+ }
170
+ return sections.length > 0 || sensitivityChanged;
171
+ }
75
172
  async resourcesTableLog(resources) {
76
173
  cli_1.default.table(resources, {
77
174
  name: {
@@ -1,5 +1,12 @@
1
1
  import { Variable } from '../types';
2
2
  import ResourceCommand from './resource-command';
3
+ import { DiffSection, ResourcePair } from '../../services/resource-differ';
3
4
  export default abstract class VariablesCommand extends ResourceCommand<Variable> {
4
5
  init(): Promise<void>;
6
+ /**
7
+ * Variables share their files, so they diff as one patch per file rather than one per key.
8
+ * Rendered from every variable, not just the changed ones, so hunks carry the surrounding keys
9
+ * and the line numbers of the real file.
10
+ */
11
+ protected diffSections(diffable: Array<ResourcePair<Variable>>, allPairs: Array<ResourcePair<Variable>>): Array<DiffSection>;
5
12
  }
@@ -4,6 +4,7 @@ const variable_1 = require("../../repositories/variable");
4
4
  const resource_merger_1 = require("../resource-merger");
5
5
  const resource_pusher_1 = require("../resource-pusher");
6
6
  const resource_command_1 = require("./resource-command");
7
+ const resource_differ_1 = require("../../services/resource-differ");
7
8
  class VariablesCommand extends resource_command_1.default {
8
9
  async init() {
9
10
  await super.init();
@@ -11,6 +12,18 @@ class VariablesCommand extends resource_command_1.default {
11
12
  this._resourceMerger = new resource_merger_1.default(this._resourceRepository);
12
13
  this._resourcePusher = new resource_pusher_1.default(this._resourceRepository, this._fcodeGraphQLClient);
13
14
  this._resourceQuery = () => this._fcodeGraphQLClient.allVariables.bind(this._fcodeGraphQLClient)();
15
+ this._diffFilesFor = (pair) => (0, resource_differ_1.variablesEnvDiffFiles)([pair]);
16
+ }
17
+ /**
18
+ * Variables share their files, so they diff as one patch per file rather than one per key.
19
+ * Rendered from every variable, not just the changed ones, so hunks carry the surrounding keys
20
+ * and the line numbers of the real file.
21
+ */
22
+ diffSections(diffable, allPairs) {
23
+ if (diffable.length === 0) {
24
+ return [];
25
+ }
26
+ return [{ label: resource_differ_1.VARIABLES_DIFF_PATH, files: (0, resource_differ_1.variablesEnvDiffFiles)(allPairs) }];
14
27
  }
15
28
  }
16
29
  exports.default = VariablesCommand;
@@ -25,7 +25,10 @@ class EnvDependenciesCommand extends api_client_command_1.default {
25
25
  fs.rmSync(this.envPath, { recursive: true });
26
26
  }
27
27
  const manifest = await this.getManifest();
28
- const installationPath = this.installationPath(manifest.dependencies);
28
+ // The effective set — own dependencies over inherited ones — because that is what the platform
29
+ // installs, and the directory is named after the hash of what is installed.
30
+ const dependencies = manifest.effectiveDependencies;
31
+ const installationPath = this.installationPath(dependencies);
29
32
  if (fs.existsSync(installationPath)) {
30
33
  if (force) {
31
34
  fs.rmSync(installationPath, { recursive: true });
@@ -38,12 +41,12 @@ class EnvDependenciesCommand extends api_client_command_1.default {
38
41
  await this.createEnv();
39
42
  cli_1.default.log(`Installing Factorial Code ${(0, types_1.programmingLanguageDisplayName)(this.programmingLanguage)} dependencies... (This process might take some time ⌛️)`);
40
43
  cli_1.default.log('====================================');
41
- await this.installDependencies(installationPath, manifest.dependencies);
44
+ await this.installDependencies(installationPath, dependencies);
42
45
  cli_1.default.log('====================================');
43
46
  cli_1.default.log(color_1.default.green(`Factorial Code ${(0, types_1.programmingLanguageDisplayName)(this.programmingLanguage)} dependencies successfully installed in ${installationPath} 🤩\n\n`));
44
47
  }
45
48
  async check() {
46
- const installationPath = this.installationPath((await this.getManifest()).dependencies);
49
+ const installationPath = this.installationPath((await this.getManifest()).effectiveDependencies);
47
50
  if (!fs.existsSync(installationPath)) {
48
51
  cli_1.default.log(color_1.default.yellow(`⚠️ ${(0, types_1.programmingLanguageDisplayName)(this.programmingLanguage)} dependencies need to be installed.`));
49
52
  }
@@ -593,13 +593,44 @@ const main = typeof _hooksModule['${fn}'] === 'function'
593
593
  }
594
594
  };
595
595
  })(),
596
- // The third argument is the version options the cloud honours. A workspace directory holds
597
- // no published snapshots, so a pinned call resolves against the files on disk — the same
598
- // per-file fallback the cloud applies to a locale that was never published at that tag.
596
+ // A workspace directory holds no published snapshots, so a pinned call resolves against
597
+ // the files on disk — the same per-file fallback the cloud applies to a locale that was
598
+ // never published at that tag. A locale option resolves against that locale's own files,
599
+ // falling back key by key to the merged translations, exactly like the cloud executors,
600
+ // shortening a locale nothing matches the same way they do.
599
601
  // Own properties only and no array arguments, exactly like the cloud executors.
600
- i18n: Object.assign((key, args, _options) => {
602
+ i18n: Object.assign((key, args, options) => {
601
603
  const translations = ${JSON.stringify(metadata.i18n.translations, null, 2)};
602
- const template = Object.hasOwn(translations, key) ? translations[key] : undefined;
604
+ const byLocale = ${JSON.stringify(metadata.i18n.byLocale, null, 2)};
605
+ const requested = options && typeof options === 'object' && options.locale !== undefined && options.locale !== null
606
+ ? String(options.locale)
607
+ : undefined;
608
+ // An identifier no locale matches is shortened one segment at a time ('en-gb' reads
609
+ // 'en'), by name first and ignoring case only as a last resort — the same chain the
610
+ // cloud resolves, so a local run answers a regional variant the same way.
611
+ const localeMaps = [];
612
+ for (let candidate = requested; candidate; ) {
613
+ const names = Object.keys(byLocale);
614
+ const lowered = candidate.toLowerCase();
615
+ const match = names.includes(candidate)
616
+ ? candidate
617
+ : names.filter((name) => name.toLowerCase() === lowered).sort()[0];
618
+ if (match !== undefined && !localeMaps.includes(byLocale[match])) {
619
+ localeMaps.push(byLocale[match]);
620
+ }
621
+ const lastSeparator = Math.max(candidate.lastIndexOf('-'), candidate.lastIndexOf('_'));
622
+ candidate = lastSeparator > 0 ? candidate.slice(0, lastSeparator) : undefined;
623
+ }
624
+ let template;
625
+ for (const localeMap of localeMaps) {
626
+ template = Object.hasOwn(localeMap, key) ? localeMap[key] : undefined;
627
+ if (template !== undefined) {
628
+ break;
629
+ }
630
+ }
631
+ if (template === undefined) {
632
+ template = Object.hasOwn(translations, key) ? translations[key] : undefined;
633
+ }
603
634
  if (template === undefined) {
604
635
  return key;
605
636
  }
@@ -101,6 +101,7 @@ export type MergedResource = MergedResourceRecord & {
101
101
  status: ResourceStatus;
102
102
  };
103
103
  export type ProgrammingLanguage = 'JAVASCRIPT' | 'PYTHON';
104
+ export declare const PROGRAMMING_LANGUAGES: ReadonlyArray<ProgrammingLanguage>;
104
105
  export declare const programmingLanguageDisplayName: (programmingLanguage: ProgrammingLanguage) => string;
105
106
  export type WebhookAuthMode = 'NONE' | 'TEAM' | 'CUSTOM';
106
107
  export declare const DEFAULT_WEBHOOK_AUTH_HEADER = "Authorization";
@@ -130,12 +131,30 @@ export interface ProcessForm {
130
131
  appRole?: AppRole;
131
132
  authMode?: FormAuthMode;
132
133
  }
134
+ export interface ProcessUiTrigger {
135
+ enabled: boolean;
136
+ /** Identifier of the UI location the trigger is rendered in. */
137
+ locationId?: string;
138
+ /**
139
+ * Label shown on the trigger. May contain fcode.i18n("key") tokens, which the platform
140
+ * resolves when a process listing names a locale.
141
+ */
142
+ label?: string;
143
+ /** f0 icon name, e.g. "Globe". */
144
+ icon?: string;
145
+ /**
146
+ * When true the trigger executes the process synchronously and returns the result
147
+ * envelope; otherwise the execution is fire-and-forget.
148
+ */
149
+ awaitResult?: boolean;
150
+ }
133
151
  export interface ProcessMetadataFields {
134
152
  name: string;
135
153
  description?: string;
136
154
  tags: string[];
137
155
  webhook?: ProcessWebhook;
138
156
  form?: ProcessForm;
157
+ uiTrigger?: ProcessUiTrigger;
139
158
  }
140
159
  export declare class Process extends Resource {
141
160
  id: string;
@@ -146,6 +165,7 @@ export declare class Process extends Resource {
146
165
  tags: string[];
147
166
  webhook?: ProcessWebhook;
148
167
  form?: ProcessForm;
168
+ uiTrigger?: ProcessUiTrigger;
149
169
  script: {
150
170
  parametersSchema: any;
151
171
  programmingLanguage: ProgrammingLanguage;
@@ -220,6 +240,20 @@ export declare class VersionedScriptLibrary extends Resource {
220
240
  constructor(id: string, tag: string, sourceCode: string, programmingLanguage: ProgrammingLanguage);
221
241
  get contentHash(): string;
222
242
  }
243
+ /**
244
+ * A published snapshot of the dependencies a workspace declares for one language. Pulled read-only
245
+ * for visibility and git history: unlike process and module versions, these are never pushed back —
246
+ * `fcode settings:versions:create` is what publishes them.
247
+ */
248
+ export declare class VersionedProgrammingLanguageManifest extends Resource {
249
+ id: string;
250
+ slug: string;
251
+ tag: string;
252
+ programmingLanguage: ProgrammingLanguage;
253
+ dependencies: Array<Dependency>;
254
+ constructor(tag: string, programmingLanguage: ProgrammingLanguage, dependencies: Array<Dependency>);
255
+ get contentHash(): string;
256
+ }
223
257
  export declare class Variable extends Resource {
224
258
  static ENV_KEY_MAX_LENGTH: number;
225
259
  static ENV_KEY_REGEX: RegExp;
@@ -238,7 +272,14 @@ export declare class ProgrammingLanguageManifest extends Resource {
238
272
  slug: string;
239
273
  programmingLanguage: ProgrammingLanguage;
240
274
  dependencies: Array<Dependency>;
241
- constructor(id: string, slug: string, programmingLanguage: ProgrammingLanguage, dependencies: Array<Dependency>);
275
+ inheritedDependencies: Array<Dependency>;
276
+ constructor(id: string, slug: string, programmingLanguage: ProgrammingLanguage, dependencies: Array<Dependency>, inheritedDependencies?: Array<Dependency>);
277
+ /**
278
+ * What an execution installs: this workspace's dependencies layered over the inherited ones, one
279
+ * entry per package name. Mirrors the platform's merge so a local run resolves the same install
280
+ * directory the cloud does.
281
+ */
282
+ get effectiveDependencies(): Array<Dependency>;
242
283
  get contentHash(): string;
243
284
  }
244
285
  export interface ErrorHandlerConfig {
@@ -246,8 +287,8 @@ export interface ErrorHandlerConfig {
246
287
  tag?: string | null;
247
288
  }
248
289
  export interface TeamVersionManifestEntry {
249
- resourceType: 'MODULE' | 'PROCESS';
250
- resourceId: string;
290
+ resourceType: 'DEPENDENCIES' | 'LOCALE' | 'MODULE' | 'PROCESS';
291
+ resourceId?: string | null;
251
292
  slug: string;
252
293
  outcome: 'CREATED' | 'FAILED' | 'SKIPPED_EXISTS' | 'SKIPPED_NO_TAG' | 'UPDATED';
253
294
  error?: string | null;
@@ -273,23 +314,28 @@ export interface TeamVersionAlias {
273
314
  export interface TeamVersionFileEntry {
274
315
  tag: string;
275
316
  comment?: string;
276
- createdAt: string;
317
+ createdAt?: string;
277
318
  }
278
319
  export interface TeamAliasFileEntry {
279
320
  name: string;
280
321
  tag: string;
281
322
  }
282
- export declare class Team extends Resource {
323
+ export interface ParentTeamRef {
324
+ slug: string;
325
+ version?: string;
326
+ }
327
+ /** The workspace's settings.json: what `fcode settings:pull|push` syncs against the cloud team. */
328
+ export declare class WorkspaceSettings extends Resource {
283
329
  id: string | undefined;
284
330
  slug: string;
285
331
  zoneId?: string;
286
332
  errorHandlerConfig?: ErrorHandlerConfig;
287
- parentTeamSlugs: string[];
333
+ parentTeams: ParentTeamRef[];
288
334
  webhookAuth?: WebhookAuth;
289
335
  primaryLocale?: string;
290
336
  versions?: TeamVersionFileEntry[];
291
337
  aliases?: TeamAliasFileEntry[];
292
- constructor(slug: string, parentTeamSlugs: string[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig, webhookAuth?: WebhookAuth, primaryLocale?: string);
338
+ constructor(slug: string, parentTeams: ParentTeamRef[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig, webhookAuth?: WebhookAuth, primaryLocale?: string);
293
339
  get contentHash(): string;
294
340
  }
295
341
  export interface RawErrorHandlerConfig {
@@ -300,13 +346,13 @@ export interface RawTeamSettings {
300
346
  slug: string;
301
347
  zoneId?: string;
302
348
  errorHandlerConfig?: RawErrorHandlerConfig | null;
303
- parentTeamSlugs: string[];
349
+ parentTeams: ParentTeamRef[];
304
350
  webhookAuth?: WebhookAuth | null;
305
351
  primaryLocale?: string | null;
306
352
  }
307
353
  export interface TeamUpdateInput {
308
354
  zoneId?: string;
309
- parentTeamSlugs?: string[];
355
+ parentTeams?: ParentTeamRef[];
310
356
  executionErrorHandlerConfig?: RawErrorHandlerConfig | null;
311
357
  webhookAuth?: {
312
358
  headerName?: string | null;
@@ -352,6 +398,9 @@ export declare const ForceFlag: {
352
398
  export declare const ResourceSlugArg: {
353
399
  resourceSlug: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
354
400
  };
401
+ export declare const ShowInheritedFlag: {
402
+ showInherited: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
403
+ };
355
404
  export declare const TeamNameArg: {
356
405
  team: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
357
406
  };
@@ -363,6 +412,7 @@ export declare const DependenciesFlags: {
363
412
  export declare class Dependency {
364
413
  name: string;
365
414
  specifier: string;
415
+ resolvingTeamSlug?: string;
366
416
  }
367
417
  export declare class ServiceAccount extends Resource {
368
418
  id: string;
@@ -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.ResourceSlugArg = exports.ForceFlag = exports.ScriptLibraryNotFound = exports.ProcessNotFound = exports.Team = exports.ProgrammingLanguageManifest = exports.Variable = exports.VersionedScriptLibrary = exports.Locale = exports.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = 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.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");
@@ -28,6 +28,7 @@ var ResourceStatus;
28
28
  ResourceStatus["SENSITIVE"] = "\uD83D\uDD12 (sensitive)";
29
29
  ResourceStatus["SENSITIVITY_CHANGED"] = "\uD83D\uDEAB (sensitivity-changed)";
30
30
  })(ResourceStatus || (exports.ResourceStatus = ResourceStatus = {}));
31
+ exports.PROGRAMMING_LANGUAGES = ['JAVASCRIPT', 'PYTHON'];
31
32
  const programmingLanguageDisplayName = (programmingLanguage) => {
32
33
  switch (programmingLanguage) {
33
34
  case 'JAVASCRIPT':
@@ -49,6 +50,7 @@ class Process extends Resource {
49
50
  this.tags = metadata.tags;
50
51
  this.webhook = metadata.webhook;
51
52
  this.form = metadata.form;
53
+ this.uiTrigger = metadata.uiTrigger;
52
54
  this.script = script;
53
55
  this.readme = readme;
54
56
  this.manifest = settings.dependencies.scopedToProcess && manifest !== null ? manifest : { dependencies: [] };
@@ -65,6 +67,9 @@ class Process extends Resource {
65
67
  tags: this.tags || [],
66
68
  webhook: this.webhook || null,
67
69
  form: this.form || null,
70
+ // Inserted only when set, so every process that predates UI triggers keeps the
71
+ // content hash already stored in .fcode (mirrors WorkspaceSettings.contentHash).
72
+ ...(this.uiTrigger && { uiTrigger: this.uiTrigger }),
68
73
  manifestDependencies: this.manifest.dependencies,
69
74
  dependenciesScopedToProcess: this.settings.dependencies.scopedToProcess,
70
75
  });
@@ -152,6 +157,25 @@ class VersionedScriptLibrary extends Resource {
152
157
  }
153
158
  }
154
159
  exports.VersionedScriptLibrary = VersionedScriptLibrary;
160
+ /**
161
+ * A published snapshot of the dependencies a workspace declares for one language. Pulled read-only
162
+ * for visibility and git history: unlike process and module versions, these are never pushed back —
163
+ * `fcode settings:versions:create` is what publishes them.
164
+ */
165
+ class VersionedProgrammingLanguageManifest extends Resource {
166
+ constructor(tag, programmingLanguage, dependencies) {
167
+ super();
168
+ this.id = `${tag}#${programmingLanguage}`;
169
+ this.slug = programmingLanguage;
170
+ this.tag = tag;
171
+ this.programmingLanguage = programmingLanguage;
172
+ this.dependencies = dependencies;
173
+ }
174
+ get contentHash() {
175
+ return crypto.createHash('md5').update(JSON.stringify(this.dependencies)).digest('hex');
176
+ }
177
+ }
178
+ exports.VersionedProgrammingLanguageManifest = VersionedProgrammingLanguageManifest;
155
179
  class Variable extends Resource {
156
180
  constructor(id, key, value, isSensitive, resolvingTeamSlug) {
157
181
  super();
@@ -185,12 +209,22 @@ exports.Variable = Variable;
185
209
  Variable.ENV_KEY_MAX_LENGTH = 64;
186
210
  Variable.ENV_KEY_REGEX = /^[A-Z_a-z]\w*$/;
187
211
  class ProgrammingLanguageManifest extends Resource {
188
- constructor(id, slug, programmingLanguage, dependencies) {
212
+ constructor(id, slug, programmingLanguage, dependencies, inheritedDependencies = []) {
189
213
  super();
190
214
  this.id = id;
191
215
  this.slug = slug;
192
216
  this.programmingLanguage = programmingLanguage;
193
217
  this.dependencies = dependencies;
218
+ this.inheritedDependencies = inheritedDependencies;
219
+ }
220
+ /**
221
+ * What an execution installs: this workspace's dependencies layered over the inherited ones, one
222
+ * entry per package name. Mirrors the platform's merge so a local run resolves the same install
223
+ * directory the cloud does.
224
+ */
225
+ get effectiveDependencies() {
226
+ const ownNames = new Set(this.dependencies.map((dependency) => dependency.name));
227
+ return [...this.dependencies, ...this.inheritedDependencies.filter((dependency) => !ownNames.has(dependency.name))];
194
228
  }
195
229
  get contentHash() {
196
230
  return crypto
@@ -200,11 +234,12 @@ class ProgrammingLanguageManifest extends Resource {
200
234
  }
201
235
  }
202
236
  exports.ProgrammingLanguageManifest = ProgrammingLanguageManifest;
203
- class Team extends Resource {
204
- constructor(slug, parentTeamSlugs, zoneId, errorHandlerConfig, webhookAuth, primaryLocale) {
237
+ /** The workspace's settings.json: what `fcode settings:pull|push` syncs against the cloud team. */
238
+ class WorkspaceSettings extends Resource {
239
+ constructor(slug, parentTeams, zoneId, errorHandlerConfig, webhookAuth, primaryLocale) {
205
240
  super();
206
241
  this.slug = slug;
207
- this.parentTeamSlugs = parentTeamSlugs;
242
+ this.parentTeams = parentTeams;
208
243
  this.zoneId = zoneId;
209
244
  this.errorHandlerConfig = errorHandlerConfig;
210
245
  this.webhookAuth = webhookAuth;
@@ -215,24 +250,38 @@ class Team extends Resource {
215
250
  zoneId: this.zoneId || null,
216
251
  errorHandlerConfig: this.errorHandlerConfig || null,
217
252
  // Kept in the given order, not sorted: the order is the inheritance precedence, so a pure
218
- // reorder is a real change that must be detected and pushed.
219
- parentTeamSlugs: [...(this.parentTeamSlugs || [])],
253
+ // reorder is a real change that must be detected and pushed. Renamed from parentTeamSlugs
254
+ // when version pins arrived, so every workspace with parents reports settings.json modified
255
+ // once after upgrading — a deliberate one-time diff.
256
+ parentTeams: (this.parentTeams || []).map(({ slug, version }) => version ? { slug, version } : { slug }),
220
257
  };
221
- // Inserted only when set, so every team.json that predates webhook auth keeps the
258
+ // Inserted only when set, so every settings.json that predates webhook auth keeps the
222
259
  // hash already stored in .fcode and does not report a phantom conflict. A key present
223
260
  // with an undefined value would not do: canonicalStringify writes that as null.
224
261
  if (this.webhookAuth) {
225
262
  payload.webhookAuth = this.webhookAuth;
226
263
  }
227
- // Same insert-only-when-set rule, for the same reason: a team.json written before locales
264
+ // Same insert-only-when-set rule, for the same reason: a settings.json written before locales
228
265
  // existed must keep the hash already stored in .fcode.
229
266
  if (this.primaryLocale) {
230
267
  payload.primaryLocale = this.primaryLocale;
231
268
  }
269
+ // Versions hash only their tags and aliases their name->tag pairs: createdAt is
270
+ // server-assigned and a comment is immutable once published (there is no update mutation),
271
+ // so neither can represent a pushable local change. Inserted only when non-empty, again to
272
+ // keep hashes stored before versioning existed valid.
273
+ if (this.versions && this.versions.length > 0) {
274
+ payload.versions = this.versions.map(({ tag }) => tag).sort();
275
+ }
276
+ if (this.aliases && this.aliases.length > 0) {
277
+ payload.aliases = this.aliases
278
+ .map(({ name, tag }) => ({ name, tag }))
279
+ .sort((a, b) => a.name.localeCompare(b.name));
280
+ }
232
281
  return (0, canonical_hash_1.canonicalJsonHash)(payload);
233
282
  }
234
283
  }
235
- exports.Team = Team;
284
+ exports.WorkspaceSettings = WorkspaceSettings;
236
285
  class ProcessNotFound extends Error {
237
286
  }
238
287
  exports.ProcessNotFound = ProcessNotFound;
@@ -251,6 +300,11 @@ exports.ResourceSlugArg = {
251
300
  name: 'slug',
252
301
  }),
253
302
  };
303
+ exports.ShowInheritedFlag = {
304
+ showInherited: core_1.Flags.boolean({
305
+ description: 'Also list inherited resources that have no changes',
306
+ }),
307
+ };
254
308
  exports.TeamNameArg = {
255
309
  team: core_1.Args.string({ description: 'Team name', name: 'team' }),
256
310
  };