@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
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.flattenTranslations = flattenTranslations;
4
+ exports.localeFallbackChain = localeFallbackChain;
4
5
  exports.resolveLocalTranslations = resolveLocalTranslations;
5
6
  const fs = require("fs-extra");
6
7
  const path = require("node:path");
@@ -8,6 +9,7 @@ const yaml_1 = require("yaml");
8
9
  const cli_1 = require("../utils/cli");
9
10
  const color_1 = require("@oclif/color");
10
11
  const types_1 = require("../core/types");
12
+ const settings_1 = require("../repositories/settings");
11
13
  const I18N_DIRECTORY = 'i18n';
12
14
  const YAML_EXTENSION = '.yaml';
13
15
  const KEY_SEPARATOR = '.';
@@ -71,8 +73,9 @@ function readLocale(workspaceDir, locale) {
71
73
  }
72
74
  function chosenPrimaryLocaleOf(workspaceDir) {
73
75
  try {
74
- const team = fs.readJsonSync(path.join(workspaceDir, 'team.json'));
75
- return typeof team?.primaryLocale === 'string' && team.primaryLocale.length > 0 ? team.primaryLocale : undefined;
76
+ (0, settings_1.migrateLegacySettingsFile)(workspaceDir);
77
+ const settings = fs.readJsonSync(path.join(workspaceDir, settings_1.SETTINGS_FILENAME));
78
+ return typeof settings?.primaryLocale === 'string' && settings.primaryLocale.length > 0 ? settings.primaryLocale : undefined;
76
79
  }
77
80
  catch {
78
81
  return undefined;
@@ -95,7 +98,7 @@ function availableLocales(workspaceDir) {
95
98
  return [...new Set(localeFileSlugs(workspaceDir).map((slug) => identifierOf(slug)))].sort();
96
99
  }
97
100
  /**
98
- * The workspace's primary locale: the one chosen in `team.json`, or its first locale when none is
101
+ * The workspace's primary locale: the one chosen in `settings.json`, or its first locale when none is
99
102
  * chosen — or when the chosen one has no file anymore, which would otherwise resolve zero keys.
100
103
  * Mirrors the platform's `PrimaryLocale.resolve`, so a local run and the cloud agree on which
101
104
  * locale a process without an explicit one resolves against.
@@ -105,16 +108,68 @@ function primaryLocaleOf(workspaceDir) {
105
108
  const chosen = chosenPrimaryLocaleOf(workspaceDir);
106
109
  return chosen && available.includes(chosen) ? chosen : available[0];
107
110
  }
111
+ /**
112
+ * The available locale a candidate opens: the one named exactly, or — only when no name matches —
113
+ * the one that differs from it in case alone, alphabetically first if there are several. Locale
114
+ * identifiers are case-sensitive, so `pt-BR` and `pt-br` are two files and each keeps answering its
115
+ * own name; ignoring case only ever rescues a lookup that would otherwise find nothing.
116
+ */
117
+ function matchingLocale(candidate, available) {
118
+ if (available.includes(candidate)) {
119
+ return candidate;
120
+ }
121
+ return available.filter((locale) => locale.toLowerCase() === candidate.toLowerCase()).sort()[0];
122
+ }
123
+ /**
124
+ * The locale files that answer a requested identifier, lowest precedence first. Mirrors the
125
+ * platform's `LocaleFallbackChain`, so a local run and the cloud resolve the same text.
126
+ *
127
+ * A viewer's locale is usually a regional variant (`en-gb`, `pt-BR`) while a workspace usually
128
+ * publishes plain languages, so the request degrades one segment at a time — split on `-` or `_`,
129
+ * without parsing a language tag, since any short slug is a legal identifier here — and every step
130
+ * that matches stays in the chain: a variant file translates the keys that differ and leaves the
131
+ * rest to the language it came from.
132
+ */
133
+ function localeFallbackChain(requested, primary, available) {
134
+ const chain = [];
135
+ const add = (candidate) => {
136
+ const match = candidate ? matchingLocale(candidate, available) : undefined;
137
+ if (match && !chain.includes(match)) {
138
+ chain.push(match);
139
+ }
140
+ };
141
+ add(primary);
142
+ const candidates = [];
143
+ let candidate = requested?.trim();
144
+ while (candidate) {
145
+ candidates.unshift(candidate);
146
+ const lastSeparator = Math.max(candidate.lastIndexOf('-'), candidate.lastIndexOf('_'));
147
+ candidate = lastSeparator > 0 ? candidate.slice(0, lastSeparator) : undefined;
148
+ }
149
+ for (const shorterOrExact of candidates)
150
+ add(shorterOrExact);
151
+ return chain;
152
+ }
108
153
  /**
109
154
  * @param requestedLocale the locale to run with, or undefined to use the workspace's primary
110
- * @returns the locale actually used and its flat translations, with the primary locale underneath
155
+ * @returns the locale actually used and its flat translations, with the primary locale — and any
156
+ * shorter form of the requested one the workspace publishes — layered underneath, plus each
157
+ * locale's own map (inherited file layered under its own, but NOT merged over the primary) so a
158
+ * call naming a locale (`fcode.i18n(key, args, { locale })`) resolves like the cloud's `by_locale`:
159
+ * key by key in the requested locale first, falling back to the merged translations
111
160
  */
112
161
  function resolveLocalTranslations(requestedLocale, workspaceDir = process.cwd()) {
113
162
  const primary = primaryLocaleOf(workspaceDir);
114
163
  const locale = requestedLocale || primary;
164
+ const byLocale = Object.fromEntries(availableLocales(workspaceDir).map((slug) => [slug, readLocale(workspaceDir, slug)]));
115
165
  if (!locale) {
116
- return { locale: undefined, translations: {} };
166
+ return { locale: undefined, translations: {}, byLocale };
167
+ }
168
+ // `byLocale` already holds every locale's own file, so the merged map — the fallback chain
169
+ // layered lowest precedence first — is derived from it rather than read from disk a second time.
170
+ const translations = {};
171
+ for (const layer of localeFallbackChain(requestedLocale, primary, Object.keys(byLocale))) {
172
+ Object.assign(translations, byLocale[layer]);
117
173
  }
118
- const fallback = primary && primary !== locale ? readLocale(workspaceDir, primary) : {};
119
- return { locale, translations: { ...fallback, ...readLocale(workspaceDir, locale) } };
174
+ return { locale, translations, byLocale };
120
175
  }
@@ -0,0 +1,60 @@
1
+ import { Locale, Process, ProgrammingLanguageManifest, Resource, ResourceStatus, ScriptLibrary, Variable } from '../core/types';
2
+ import { PseudoFile } from '../utils/diff';
3
+ /** Stands in for a variable's value wherever one side declares it sensitive, so no secret is ever printed. */
4
+ export declare const SENSITIVE_PLACEHOLDER = "<sensitive value hidden>";
5
+ export declare const VARIABLES_DIFF_PATH = "variables.env";
6
+ /** What a workspace inherits is regenerated on pull into its own file, which the parent owns. */
7
+ export declare const INHERITED_VARIABLES_DIFF_PATH = "variables.inherited.env";
8
+ export declare const SETTINGS_DIFF_PATH = "settings.json";
9
+ /**
10
+ * A resource as both sides hold it, kept whole. `ResourceMerger.mergeStatus` collapses the two into
11
+ * one object and loses the remote body, which is exactly what a diff needs.
12
+ */
13
+ export type ResourcePair<R extends Resource> = {
14
+ slug: string;
15
+ remote?: R;
16
+ local?: R;
17
+ status: ResourceStatus;
18
+ };
19
+ /** A set of pseudo-files presented together under one heading (usually one resource). */
20
+ export type DiffSection = {
21
+ label: string;
22
+ files: PseudoFile[];
23
+ };
24
+ /** Files keyed by the path they are stored at, for one side of a comparison. */
25
+ type SideFiles = Record<string, string>;
26
+ export declare function pairResources<R extends Resource>(remoteResources: Array<R>, localResources: Array<R>, existsMetadata: (slug: string) => Promise<boolean>): Promise<Array<ResourcePair<R>>>;
27
+ /**
28
+ * The workspace a resource is inherited from, when it is. An overridden variable resolves to
29
+ * undefined on its local side, which is what makes an override diffable while the parent's copy
30
+ * stays read-only.
31
+ */
32
+ export declare function inheritedFrom<R extends Resource>(pair: ResourcePair<R>): string | undefined;
33
+ /** A process as the four files it is stored as, plus its own dependencies file when it scopes them. */
34
+ export declare function processFiles(process: Process): SideFiles;
35
+ export declare function moduleFiles(module: ScriptLibrary): SideFiles;
36
+ export declare function localeFiles(locale: Locale): SideFiles;
37
+ /**
38
+ * Only the workspace's own dependencies, matching what `contentHash` covers and what the manifest
39
+ * file holds: the inherited ones live in their own file and belong to a parent.
40
+ */
41
+ export declare function manifestFiles(manifest: ProgrammingLanguageManifest): SideFiles;
42
+ export declare const processDiffFiles: (pair: ResourcePair<Process>) => Array<PseudoFile>;
43
+ export declare const moduleDiffFiles: (pair: ResourcePair<ScriptLibrary>) => Array<PseudoFile>;
44
+ export declare const localeDiffFiles: (pair: ResourcePair<Locale>) => Array<PseudoFile>;
45
+ export declare const manifestDiffFiles: (pair: ResourcePair<ProgrammingLanguageManifest>) => Array<PseudoFile>;
46
+ /**
47
+ * Every variable as the .env files they share, rather than one pseudo-file each: that is how they
48
+ * are stored, and it keeps a rename or a reordering readable as one patch.
49
+ *
50
+ * The workspace's own keys and the inherited ones are separate files, because they are separate
51
+ * files on disk — a drifted inherited variable is a stale `variables.inherited.env` that a pull
52
+ * regenerates, not something to push. The inherited file only appears when something is inherited.
53
+ *
54
+ * A key either side marks sensitive is rendered as a placeholder on both, so a diff never prints a
55
+ * secret and a value-only change to a sensitive variable shows up as nothing (its status does).
56
+ */
57
+ export declare function variablesEnvDiffFiles(pairs: Array<ResourcePair<Variable>>): Array<PseudoFile>;
58
+ /** The singleton workspace settings file. Both sides arrive already serialized by `SettingsRepository.toFile`. */
59
+ export declare function settingsDiffFile(remote: Record<string, any> | undefined, local: Record<string, any> | undefined): PseudoFile;
60
+ export {};
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.manifestDiffFiles = exports.localeDiffFiles = exports.moduleDiffFiles = exports.processDiffFiles = exports.SETTINGS_DIFF_PATH = exports.INHERITED_VARIABLES_DIFF_PATH = exports.VARIABLES_DIFF_PATH = exports.SENSITIVE_PLACEHOLDER = void 0;
4
+ exports.pairResources = pairResources;
5
+ exports.inheritedFrom = inheritedFrom;
6
+ exports.processFiles = processFiles;
7
+ exports.moduleFiles = moduleFiles;
8
+ exports.localeFiles = localeFiles;
9
+ exports.manifestFiles = manifestFiles;
10
+ exports.variablesEnvDiffFiles = variablesEnvDiffFiles;
11
+ exports.settingsDiffFile = settingsDiffFile;
12
+ const resource_merger_1 = require("../core/resource-merger");
13
+ const dependencies_1 = require("../core/utils/dependencies");
14
+ const process_1 = require("../repositories/process");
15
+ const env_utils_1 = require("../utils/env-utils");
16
+ const filenamify_1 = require("../utils/filenamify");
17
+ /** Stands in for a variable's value wherever one side declares it sensitive, so no secret is ever printed. */
18
+ exports.SENSITIVE_PLACEHOLDER = '<sensitive value hidden>';
19
+ exports.VARIABLES_DIFF_PATH = 'variables.env';
20
+ /** What a workspace inherits is regenerated on pull into its own file, which the parent owns. */
21
+ exports.INHERITED_VARIABLES_DIFF_PATH = 'variables.inherited.env';
22
+ exports.SETTINGS_DIFF_PATH = 'settings.json';
23
+ async function pairResources(remoteResources, localResources, existsMetadata) {
24
+ const pairs = [];
25
+ const slugs = [...new Set([...localResources, ...remoteResources].map((resource) => resource.slug))];
26
+ for (const slug of slugs) {
27
+ const remote = remoteResources.find((resource) => resource.slug === slug);
28
+ const local = localResources.find((resource) => resource.slug === slug);
29
+ pairs.push({ slug, remote, local, status: await (0, resource_merger_1.mapResourceStatus)(remote, local, await existsMetadata(slug)) });
30
+ }
31
+ return pairs;
32
+ }
33
+ /**
34
+ * The workspace a resource is inherited from, when it is. An overridden variable resolves to
35
+ * undefined on its local side, which is what makes an override diffable while the parent's copy
36
+ * stays read-only.
37
+ */
38
+ function inheritedFrom(pair) {
39
+ return (pair.local ?? pair.remote)?.resolvingTeamSlug;
40
+ }
41
+ function mergeSides(remoteFiles, localFiles) {
42
+ const paths = [...new Set([...Object.keys(remoteFiles), ...Object.keys(localFiles)])].sort();
43
+ return paths.map((path) => {
44
+ const file = { path };
45
+ if (path in remoteFiles) {
46
+ file.remote = remoteFiles[path];
47
+ }
48
+ if (path in localFiles) {
49
+ file.local = localFiles[path];
50
+ }
51
+ return file;
52
+ });
53
+ }
54
+ function sideFiles(pair, toFiles) {
55
+ return mergeSides(pair.remote ? toFiles(pair.remote) : {}, pair.local ? toFiles(pair.local) : {});
56
+ }
57
+ /** A process as the four files it is stored as, plus its own dependencies file when it scopes them. */
58
+ function processFiles(process) {
59
+ const directory = `processes/${(0, filenamify_1.default)(process.slug)}`;
60
+ const { programmingLanguage } = process.script;
61
+ const files = {
62
+ [`${directory}/${(0, process_1.PROCESS_SOURCE_CODE_FILE_NAME)(programmingLanguage)}`]: process.script.sourceCode || '',
63
+ [`${directory}/README.md`]: process.readme || '',
64
+ [`${directory}/metadata.json`]: JSON.stringify((0, process_1.buildProcessMetadata)(process), null, 2),
65
+ [`${directory}/parametersSchema.json`]: JSON.stringify(process.script.parametersSchema ?? {}, null, 2),
66
+ };
67
+ if (process.settings?.dependencies?.scopedToProcess) {
68
+ files[`${directory}/${(0, process_1.PROCESS_DEPENDENCIES_FILE_NAME)(programmingLanguage)}`] = (0, dependencies_1.dependenciesToFile)(programmingLanguage, process.manifest?.dependencies);
69
+ }
70
+ return files;
71
+ }
72
+ function moduleFiles(module) {
73
+ const name = (0, filenamify_1.default)(module.name);
74
+ const extension = module.programmingLanguage === 'PYTHON' ? 'py' : 'js';
75
+ return { [`modules/${name}/${name}.${extension}`]: module.sourceCode || '' };
76
+ }
77
+ function localeFiles(locale) {
78
+ return { [`i18n/${locale.slug}.yaml`]: locale.content || '' };
79
+ }
80
+ /**
81
+ * Only the workspace's own dependencies, matching what `contentHash` covers and what the manifest
82
+ * file holds: the inherited ones live in their own file and belong to a parent.
83
+ */
84
+ function manifestFiles(manifest) {
85
+ const fileName = manifest.programmingLanguage === 'JAVASCRIPT' ? 'package.json' : 'requirements.txt';
86
+ return { [`dependencies/${fileName}`]: (0, dependencies_1.dependenciesToFile)(manifest.programmingLanguage, manifest.dependencies) };
87
+ }
88
+ const processDiffFiles = (pair) => sideFiles(pair, processFiles);
89
+ exports.processDiffFiles = processDiffFiles;
90
+ const moduleDiffFiles = (pair) => sideFiles(pair, moduleFiles);
91
+ exports.moduleDiffFiles = moduleDiffFiles;
92
+ const localeDiffFiles = (pair) => sideFiles(pair, localeFiles);
93
+ exports.localeDiffFiles = localeDiffFiles;
94
+ const manifestDiffFiles = (pair) => sideFiles(pair, manifestFiles);
95
+ exports.manifestDiffFiles = manifestDiffFiles;
96
+ function renderVariablesFile(pairs, path, include) {
97
+ const sensitiveKeys = new Set(pairs.filter((pair) => pair.remote?.isSensitive || pair.local?.isSensitive).map((pair) => pair.slug));
98
+ const renderSide = (side) => pairs
99
+ .map(side)
100
+ .filter((variable) => variable !== undefined && include(variable))
101
+ .sort((a, b) => a.key.localeCompare(b.key))
102
+ .map((variable) => `${variable.key}=${sensitiveKeys.has(variable.key) ? exports.SENSITIVE_PLACEHOLDER : (0, env_utils_1.escapeEnvValue)(variable.value || '')}`)
103
+ .join('\n');
104
+ return { path, remote: renderSide((pair) => pair.remote), local: renderSide((pair) => pair.local) };
105
+ }
106
+ /**
107
+ * Every variable as the .env files they share, rather than one pseudo-file each: that is how they
108
+ * are stored, and it keeps a rename or a reordering readable as one patch.
109
+ *
110
+ * The workspace's own keys and the inherited ones are separate files, because they are separate
111
+ * files on disk — a drifted inherited variable is a stale `variables.inherited.env` that a pull
112
+ * regenerates, not something to push. The inherited file only appears when something is inherited.
113
+ *
114
+ * A key either side marks sensitive is rendered as a placeholder on both, so a diff never prints a
115
+ * secret and a value-only change to a sensitive variable shows up as nothing (its status does).
116
+ */
117
+ function variablesEnvDiffFiles(pairs) {
118
+ const isInherited = (variable) => Boolean(variable.resolvingTeamSlug);
119
+ const files = [renderVariablesFile(pairs, exports.VARIABLES_DIFF_PATH, (variable) => !isInherited(variable))];
120
+ if (pairs.some((pair) => (pair.remote && isInherited(pair.remote)) || (pair.local && isInherited(pair.local)))) {
121
+ files.push(renderVariablesFile(pairs, exports.INHERITED_VARIABLES_DIFF_PATH, isInherited));
122
+ }
123
+ return files;
124
+ }
125
+ /** The singleton workspace settings file. Both sides arrive already serialized by `SettingsRepository.toFile`. */
126
+ function settingsDiffFile(remote, local) {
127
+ const file = { path: exports.SETTINGS_DIFF_PATH };
128
+ if (remote !== undefined) {
129
+ file.remote = JSON.stringify(remote, null, 2);
130
+ }
131
+ if (local !== undefined) {
132
+ file.local = JSON.stringify(local, null, 2);
133
+ }
134
+ return file;
135
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Installs the Factorial Code agent skills into `targetDir`. The skills CLI writes the skills
3
+ * themselves under `.agents/skills/` and symlinks them from `.claude/skills/`; it only installs
4
+ * to agents it detects, so `.claude/` is created first to make sure Claude Code is targeted.
5
+ */
6
+ export declare function installSkillsAt(targetDir: string): Promise<void>;
7
+ export declare function skillsInstalledAt(rootDir: string): boolean;
8
+ /**
9
+ * Points `targetDir`'s `.claude/skills` at the ones installed in `rootDir`, so a team clone
10
+ * installs the skills once and every App still resolves them.
11
+ *
12
+ * The link is relative, so moving the whole tree keeps it working, and the per-skill symlinks
13
+ * inside it resolve against the real directory — not against the link — so they stay valid.
14
+ * Returns false when there is nothing to link to (the install failed, and it is not fatal).
15
+ */
16
+ export declare function linkSkillsInto(targetDir: string, rootDir: string): boolean;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installSkillsAt = installSkillsAt;
4
+ exports.skillsInstalledAt = skillsInstalledAt;
5
+ exports.linkSkillsInto = linkSkillsInto;
6
+ const node_child_process_1 = require("node:child_process");
7
+ const fs = require("fs-extra");
8
+ const path = require("node:path");
9
+ const cli_1 = require("../utils/cli");
10
+ // Skill sources installed on clone. `release-sdk` in factorial-api-sdks is a
11
+ // maintainer-only skill, so we pick `factorial-api-sdks` explicitly there.
12
+ const SKILLS_SOURCES = [
13
+ { repo: 'factorialco/factorial-code-skills', skills: '*' },
14
+ { repo: 'factorialco/factorial-api-sdks', skills: 'factorial-api-sdks' },
15
+ ];
16
+ /** Where the skills CLI symlinks the installed skills for Claude Code to find them. */
17
+ const CLAUDE_SKILLS_PATH = path.join('.claude', 'skills');
18
+ function skillsInstallHint(repo, skills) {
19
+ cli_1.default.log('💡 Install the Factorial Code agent skills later with:');
20
+ cli_1.default.log(` npx skills add ${repo} -s '${skills}' -y`);
21
+ }
22
+ async function installSkills(repo, skills, cwd) {
23
+ // `npx --yes skills add <repo> -s <skills> -y` — non-interactive install with
24
+ // the skills CLI defaults: project scope (no -g), symlinked (no --copy).
25
+ const skillsArgs = ['--yes', 'skills', 'add', repo, '-s', skills, '-y'];
26
+ await new Promise((resolve) => {
27
+ const child = (0, node_child_process_1.spawn)('npx', skillsArgs, { cwd, stdio: 'inherit' });
28
+ child.on('error', () => {
29
+ // npx not available (or failed to launch) — don't fail the clone, just tell the user how to do it.
30
+ cli_1.default.log("\n⚠️ Couldn't run npx automatically (is Node.js / npx installed?).");
31
+ skillsInstallHint(repo, skills);
32
+ resolve();
33
+ });
34
+ child.on('close', (code) => {
35
+ if (code !== 0) {
36
+ cli_1.default.log("\n⚠️ Skill installation didn't complete.");
37
+ skillsInstallHint(repo, skills);
38
+ }
39
+ resolve();
40
+ });
41
+ });
42
+ }
43
+ /**
44
+ * Installs the Factorial Code agent skills into `targetDir`. The skills CLI writes the skills
45
+ * themselves under `.agents/skills/` and symlinks them from `.claude/skills/`; it only installs
46
+ * to agents it detects, so `.claude/` is created first to make sure Claude Code is targeted.
47
+ */
48
+ async function installSkillsAt(targetDir) {
49
+ cli_1.default.log('');
50
+ fs.ensureDirSync(path.join(targetDir, '.claude'));
51
+ for (const source of SKILLS_SOURCES) {
52
+ await installSkills(source.repo, source.skills, targetDir);
53
+ }
54
+ }
55
+ function skillsInstalledAt(rootDir) {
56
+ return fs.pathExistsSync(path.join(rootDir, CLAUDE_SKILLS_PATH));
57
+ }
58
+ /**
59
+ * Points `targetDir`'s `.claude/skills` at the ones installed in `rootDir`, so a team clone
60
+ * installs the skills once and every App still resolves them.
61
+ *
62
+ * The link is relative, so moving the whole tree keeps it working, and the per-skill symlinks
63
+ * inside it resolve against the real directory — not against the link — so they stay valid.
64
+ * Returns false when there is nothing to link to (the install failed, and it is not fatal).
65
+ */
66
+ function linkSkillsInto(targetDir, rootDir) {
67
+ if (!skillsInstalledAt(rootDir)) {
68
+ return false;
69
+ }
70
+ const link = path.join(targetDir, CLAUDE_SKILLS_PATH);
71
+ if (fs.pathExistsSync(link) || fs.lstatSync(link, { throwIfNoEntry: false })) {
72
+ return true;
73
+ }
74
+ fs.ensureDirSync(path.dirname(link));
75
+ fs.symlinkSync(path.relative(path.dirname(link), path.join(rootDir, CLAUDE_SKILLS_PATH)), link, 'dir');
76
+ return true;
77
+ }
@@ -0,0 +1,16 @@
1
+ import { TeamAliasFileEntry, TeamVersion, TeamVersionAlias, TeamVersionFileEntry, WorkspaceSettings } from '../core/types';
2
+ /**
3
+ * What a settings:push must do to make the cloud's versions and aliases match settings.json.
4
+ * Versions are only ever created or deleted (tags are immutable snapshots); aliases are
5
+ * name->tag pointers, so a changed tag is an upsert of the same name.
6
+ */
7
+ export interface TeamVersioningSyncPlan {
8
+ createVersions: TeamVersionFileEntry[];
9
+ deleteVersions: TeamVersion[];
10
+ setAliases: TeamAliasFileEntry[];
11
+ deleteAliases: TeamVersionAlias[];
12
+ }
13
+ export declare function planTeamVersioningSync(settings: WorkspaceSettings, cloudVersions: TeamVersion[], cloudAliases: TeamVersionAlias[]): TeamVersioningSyncPlan;
14
+ export declare function teamVersioningSyncPlanIsEmpty(plan: TeamVersioningSyncPlan): boolean;
15
+ /** Logs the per-entity outcome of publishing a workspace version and returns how many failed. */
16
+ export declare function logTeamVersionManifest(teamVersion: TeamVersion): number;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planTeamVersioningSync = planTeamVersioningSync;
4
+ exports.teamVersioningSyncPlanIsEmpty = teamVersioningSyncPlanIsEmpty;
5
+ exports.logTeamVersionManifest = logTeamVersionManifest;
6
+ const color_1 = require("@oclif/color");
7
+ const cli_1 = require("../utils/cli");
8
+ function planTeamVersioningSync(settings, cloudVersions, cloudAliases) {
9
+ const localVersions = settings.versions ?? [];
10
+ const localAliases = settings.aliases ?? [];
11
+ const cloudVersionTags = new Set(cloudVersions.map(({ tag }) => tag));
12
+ const localVersionTags = new Set(localVersions.map(({ tag }) => tag));
13
+ const cloudAliasesByName = new Map(cloudAliases.map((alias) => [alias.name, alias]));
14
+ const localAliasNames = new Set(localAliases.map(({ name }) => name));
15
+ return {
16
+ createVersions: localVersions.filter(({ tag }) => !cloudVersionTags.has(tag)),
17
+ deleteVersions: cloudVersions.filter(({ tag }) => !localVersionTags.has(tag)),
18
+ setAliases: localAliases.filter(({ name, tag }) => cloudAliasesByName.get(name)?.tag !== tag),
19
+ deleteAliases: cloudAliases.filter(({ name }) => !localAliasNames.has(name)),
20
+ };
21
+ }
22
+ function teamVersioningSyncPlanIsEmpty(plan) {
23
+ return (plan.createVersions.length === 0 &&
24
+ plan.deleteVersions.length === 0 &&
25
+ plan.setAliases.length === 0 &&
26
+ plan.deleteAliases.length === 0);
27
+ }
28
+ /** Logs the per-entity outcome of publishing a workspace version and returns how many failed. */
29
+ function logTeamVersionManifest(teamVersion) {
30
+ const created = teamVersion.manifest.filter(({ outcome }) => outcome === 'CREATED');
31
+ const skipped = teamVersion.manifest.filter(({ outcome }) => outcome === 'SKIPPED_EXISTS');
32
+ const failed = teamVersion.manifest.filter(({ outcome }) => outcome === 'FAILED');
33
+ cli_1.default.log(color_1.default.green(`✅ ${created.length} created`) +
34
+ `, ${skipped.length} skipped (tag already exists)` +
35
+ (failed.length > 0 ? `, ${color_1.default.red(`${failed.length} failed`)}` : ''));
36
+ for (const entry of failed) {
37
+ cli_1.default.error(`${entry.resourceType.toLowerCase()} ${entry.slug} — ${entry.error ?? 'unknown error'}`);
38
+ }
39
+ return failed.length;
40
+ }
@@ -37,7 +37,7 @@ function resolveEffectiveAuth(webhook, teamWebhookAuth) {
37
37
  }
38
38
  if (authMode === 'TEAM') {
39
39
  if (!teamWebhookAuth) {
40
- throw new Error('this webhook inherits the workspace webhook auth, which is not set in team.json');
40
+ throw new Error('this webhook inherits the workspace webhook auth, which is not set in settings.json');
41
41
  }
42
42
  return teamWebhookAuth;
43
43
  }
@@ -0,0 +1,31 @@
1
+ import { TeamOrigin } from '../core/types';
2
+ /**
3
+ * Creating and syncing a workspace directory, independent of the command that asks for it, so
4
+ * `fcode clone` (one workspace) and `fcode team:clone` (one per App) share the same behaviour.
5
+ *
6
+ * Everything below a resource command resolves the workspace from `process.cwd()` — the
7
+ * repositories, `WorkspaceConfig`, and the GraphQL client bound to the workspace slug through the
8
+ * `Fcode-Team` header. `runInWorkspace` is therefore the seam: it moves into the directory,
9
+ * runs the command, and always moves back. Commands run sequentially and each `Command.run()`
10
+ * builds a fresh instance, so every workspace gets its own config and its own client.
11
+ */
12
+ /** Runs `body` with the process rooted at `workspaceDir`, restoring the previous cwd afterwards. */
13
+ export declare function runInWorkspace<T>(workspaceDir: string, body: () => Promise<T>): Promise<T>;
14
+ export declare class WorkspaceNotEmpty extends Error {
15
+ readonly workspaceDir: string;
16
+ constructor(workspaceDir: string);
17
+ }
18
+ export declare class WorkspaceAlreadyCloned extends Error {
19
+ readonly workspaceDir: string;
20
+ constructor(workspaceDir: string);
21
+ }
22
+ /** True when the directory already holds a Factorial Code workspace (`.fcode/workspace.json`). */
23
+ export declare function isClonedWorkspace(workspaceDir: string): Promise<boolean>;
24
+ /**
25
+ * Lays out an empty workspace at `workspaceDir`: the resource directories, the `.fcode` metadata
26
+ * pointing at `origin`, and the files a pull expects to already exist.
27
+ *
28
+ * Throws `WorkspaceAlreadyCloned` / `WorkspaceNotEmpty` rather than exiting, so a caller cloning
29
+ * many workspaces can report the failure and carry on with the rest.
30
+ */
31
+ export declare function scaffoldWorkspace(workspaceDir: string, origin: TeamOrigin): Promise<void>;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkspaceAlreadyCloned = exports.WorkspaceNotEmpty = void 0;
4
+ exports.runInWorkspace = runInWorkspace;
5
+ exports.isClonedWorkspace = isClonedWorkspace;
6
+ exports.scaffoldWorkspace = scaffoldWorkspace;
7
+ const fs = require("fs-extra");
8
+ const path = require("node:path");
9
+ const global_1 = require("../config/global");
10
+ const workspace_1 = require("../config/workspace");
11
+ const settings_1 = require("../config/settings");
12
+ /**
13
+ * Creating and syncing a workspace directory, independent of the command that asks for it, so
14
+ * `fcode clone` (one workspace) and `fcode team:clone` (one per App) share the same behaviour.
15
+ *
16
+ * Everything below a resource command resolves the workspace from `process.cwd()` — the
17
+ * repositories, `WorkspaceConfig`, and the GraphQL client bound to the workspace slug through the
18
+ * `Fcode-Team` header. `runInWorkspace` is therefore the seam: it moves into the directory,
19
+ * runs the command, and always moves back. Commands run sequentially and each `Command.run()`
20
+ * builds a fresh instance, so every workspace gets its own config and its own client.
21
+ */
22
+ /** Runs `body` with the process rooted at `workspaceDir`, restoring the previous cwd afterwards. */
23
+ async function runInWorkspace(workspaceDir, body) {
24
+ const previousCwd = process.cwd();
25
+ process.chdir(workspaceDir);
26
+ try {
27
+ return await body();
28
+ }
29
+ finally {
30
+ process.chdir(previousCwd);
31
+ }
32
+ }
33
+ class WorkspaceNotEmpty extends Error {
34
+ constructor(workspaceDir) {
35
+ super(`Destination path '${workspaceDir}' already exists and is not empty.`);
36
+ this.workspaceDir = workspaceDir;
37
+ }
38
+ }
39
+ exports.WorkspaceNotEmpty = WorkspaceNotEmpty;
40
+ class WorkspaceAlreadyCloned extends Error {
41
+ constructor(workspaceDir) {
42
+ super(`Workspace '${workspaceDir}' already cloned.`);
43
+ this.workspaceDir = workspaceDir;
44
+ }
45
+ }
46
+ exports.WorkspaceAlreadyCloned = WorkspaceAlreadyCloned;
47
+ /** True when the directory already holds a Factorial Code workspace (`.fcode/workspace.json`). */
48
+ async function isClonedWorkspace(workspaceDir) {
49
+ try {
50
+ await new workspace_1.default(workspaceDir).getActiveTeam();
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ async function ignoreWorkspaceNotEmpty() {
58
+ const envValue = process.env.IGNORE_WORKSPACE_NOT_EMPTY;
59
+ if (envValue === 'true' || envValue === 'false') {
60
+ return envValue === 'true';
61
+ }
62
+ return (await new global_1.default().getSetting(settings_1.SettingKeys.IGNORE_WORKSPACE_NOT_EMPTY)) === 'true';
63
+ }
64
+ /**
65
+ * Lays out an empty workspace at `workspaceDir`: the resource directories, the `.fcode` metadata
66
+ * pointing at `origin`, and the files a pull expects to already exist.
67
+ *
68
+ * Throws `WorkspaceAlreadyCloned` / `WorkspaceNotEmpty` rather than exiting, so a caller cloning
69
+ * many workspaces can report the failure and carry on with the rest.
70
+ */
71
+ async function scaffoldWorkspace(workspaceDir, origin) {
72
+ if (fs.pathExistsSync(workspaceDir)) {
73
+ // Listed up front, and `.fcode` probed only when it is already there: constructing a
74
+ // WorkspaceConfig creates the directory, which would make an empty target read as occupied.
75
+ const entries = fs.readdirSync(workspaceDir);
76
+ if (entries.includes('.fcode') && (await isClonedWorkspace(workspaceDir))) {
77
+ throw new WorkspaceAlreadyCloned(workspaceDir);
78
+ }
79
+ if (entries.length > 0 && !(await ignoreWorkspaceNotEmpty())) {
80
+ throw new WorkspaceNotEmpty(workspaceDir);
81
+ }
82
+ }
83
+ const globalConfig = new global_1.default();
84
+ const variablesFileName = await globalConfig.getSetting(settings_1.SettingKeys.VARIABLES_FILE_NAME);
85
+ const localVariablesFileName = await globalConfig.getSetting(settings_1.SettingKeys.LOCAL_VARIABLES_FILE_NAME);
86
+ fs.mkdirpSync(workspaceDir);
87
+ for (const directory of ['processes', 'modules', 'dependencies', 'i18n', 'storage']) {
88
+ fs.mkdirpSync(path.join(workspaceDir, directory));
89
+ }
90
+ fs.createFileSync(path.join(workspaceDir, variablesFileName));
91
+ fs.outputFileSync(path.join(workspaceDir, localVariablesFileName), '');
92
+ fs.outputFileSync(path.join(workspaceDir, 'datastore.json'), '{}');
93
+ fs.outputFileSync(path.join(workspaceDir, '.gitignore'), [localVariablesFileName, 'datastore.json', '.env', 'storage/*'].join('\n'));
94
+ await new workspace_1.default(workspaceDir).init(origin);
95
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Workspace slugs embed an App's UUID base36-encoded: the platform derives a Postgres database
3
+ * name from the slug and a raw UUID overflows the 63-char identifier limit, so 128 bits become
4
+ * 25 lowercase alphanumeric characters.
5
+ *
6
+ * Ported from the dashboard's `lib/slug.ts` because no REST endpoint exposes the slug — the
7
+ * dashboard builds it from the App id the same way. The round-trip test pins them together.
8
+ */
9
+ export declare function encodeUuid(uuid: string): string;
10
+ export declare function decodeUuid(token: string): string;
11
+ /** The development workspace of an App: what `fcode clone` takes and `team:clone` clones per app. */
12
+ export declare const devSlug: (appId: string) => string;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Workspace slugs embed an App's UUID base36-encoded: the platform derives a Postgres database
4
+ * name from the slug and a raw UUID overflows the 63-char identifier limit, so 128 bits become
5
+ * 25 lowercase alphanumeric characters.
6
+ *
7
+ * Ported from the dashboard's `lib/slug.ts` because no REST endpoint exposes the slug — the
8
+ * dashboard builds it from the App id the same way. The round-trip test pins them together.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.devSlug = void 0;
12
+ exports.encodeUuid = encodeUuid;
13
+ exports.decodeUuid = decodeUuid;
14
+ const TOKEN_LENGTH = 25; // ceil(128 / log2(36))
15
+ const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
16
+ function encodeUuid(uuid) {
17
+ const hex = uuid.replaceAll('-', '').toLowerCase();
18
+ if (!/^[\da-f]{32}$/.test(hex)) {
19
+ throw new Error(`Not a UUID: ${uuid}`);
20
+ }
21
+ return BigInt(`0x${hex}`).toString(36).padStart(TOKEN_LENGTH, '0');
22
+ }
23
+ function decodeUuid(token) {
24
+ let value = 0n;
25
+ for (const char of token.toLowerCase()) {
26
+ const digit = ALPHABET.indexOf(char);
27
+ if (digit === -1) {
28
+ throw new Error(`Invalid slug token: ${token}`);
29
+ }
30
+ value = value * 36n + BigInt(digit);
31
+ }
32
+ const hex = value.toString(16).padStart(32, '0');
33
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
34
+ }
35
+ /** The development workspace of an App: what `fcode clone` takes and `team:clone` clones per app. */
36
+ const devSlug = (appId) => `dev-${encodeUuid(appId)}`;
37
+ exports.devSlug = devSlug;