@factorialco/fcode-cli 1.5.0 → 2.1.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 (43) hide show
  1. package/README.md +123 -3
  2. package/dist/commands/http.d.ts +6 -4
  3. package/dist/commands/http.js +40 -24
  4. package/dist/commands/setup-debug.js +2 -2
  5. package/dist/commands/team/aliases/delete.d.ts +9 -0
  6. package/dist/commands/team/aliases/delete.js +28 -0
  7. package/dist/commands/team/aliases/list.d.ts +6 -0
  8. package/dist/commands/team/aliases/list.js +23 -0
  9. package/dist/commands/team/aliases/set.d.ts +10 -0
  10. package/dist/commands/team/aliases/set.js +25 -0
  11. package/dist/commands/team/pull.js +8 -1
  12. package/dist/commands/team/push.js +9 -1
  13. package/dist/commands/team/versions/create.d.ts +13 -0
  14. package/dist/commands/team/versions/create.js +42 -0
  15. package/dist/commands/team/versions/delete.d.ts +12 -0
  16. package/dist/commands/team/versions/delete.js +32 -0
  17. package/dist/commands/team/versions/list.d.ts +6 -0
  18. package/dist/commands/team/versions/list.js +25 -0
  19. package/dist/commands/variables/status.js +11 -0
  20. package/dist/config/settings.d.ts +1 -0
  21. package/dist/config/settings.js +6 -0
  22. package/dist/core/types.d.ts +65 -4
  23. package/dist/core/types.js +16 -5
  24. package/dist/env.d.ts +2 -2
  25. package/dist/env.js +1 -1
  26. package/dist/repositories/team.d.ts +10 -2
  27. package/dist/repositories/team.js +53 -3
  28. package/dist/repositories/variable.d.ts +24 -0
  29. package/dist/repositories/variable.js +70 -22
  30. package/dist/runtime/executor.js +2 -2
  31. package/dist/runtime/executor.py +2 -2
  32. package/dist/runtime/libs/python/fcode/__init__.py +2 -2
  33. package/dist/services/auth-service.d.ts +3 -0
  34. package/dist/services/auth-service.js +56 -5
  35. package/dist/services/fcode-graphql-client.d.ts +20 -1
  36. package/dist/services/fcode-graphql-client.js +239 -49
  37. package/dist/services/oauth-browser-service.js +1 -0
  38. package/dist/services/webhook-auth.d.ts +21 -0
  39. package/dist/services/webhook-auth.js +77 -0
  40. package/dist/utils/gitignore.d.ts +2 -0
  41. package/dist/utils/gitignore.js +6 -0
  42. package/oclif.manifest.json +208 -17
  43. package/package.json +10 -2
@@ -8,6 +8,7 @@ var SettingKeys;
8
8
  SettingKeys["IGNORE_PROCESS_VERSIONS"] = "ignoreProcessVersions";
9
9
  SettingKeys["IGNORE_WORKSPACE_NOT_EMPTY"] = "ignoreWorkspaceNotEmpty";
10
10
  SettingKeys["IGNORE_FCODE_RUN_PROCESSES"] = "ignoreFcodeRunProcesses";
11
+ SettingKeys["INHERITED_VARIABLES_FILE_NAME"] = "inheritedVariablesFileName";
11
12
  SettingKeys["LOCAL_VARIABLES_FILE_NAME"] = "localVariablesFileName";
12
13
  SettingKeys["SKIP_SKILLS_SETUP"] = "skipSkillsSetup";
13
14
  SettingKeys["VARIABLES_FILE_NAME"] = "variablesFileName";
@@ -48,6 +49,11 @@ exports.SETTINGS_DEFINITIONS = [
48
49
  default: 'variables.local.env',
49
50
  description: 'Local variables file name',
50
51
  },
52
+ {
53
+ key: SettingKeys.INHERITED_VARIABLES_FILE_NAME,
54
+ default: 'variables.inherited.env',
55
+ description: 'File holding the variables inherited from parent workspaces (read-only, gitignored)',
56
+ },
51
57
  ];
52
58
  function getSettingDefault(key) {
53
59
  return exports.SETTINGS_DEFINITIONS.find((def) => def.key === key)?.default;
@@ -10,6 +10,7 @@ export type Credentials = {
10
10
  export interface Token {
11
11
  access_token: string;
12
12
  refresh_token: string;
13
+ expires_at?: number;
13
14
  }
14
15
  export interface UserInfo {
15
16
  groups: string[];
@@ -98,15 +99,33 @@ export type MergedResource = MergedResourceRecord & {
98
99
  };
99
100
  export type ProgrammingLanguage = 'JAVASCRIPT' | 'PYTHON';
100
101
  export declare const programmingLanguageDisplayName: (programmingLanguage: ProgrammingLanguage) => string;
102
+ export type WebhookAuthMode = 'NONE' | 'TEAM' | 'CUSTOM';
103
+ export declare const DEFAULT_WEBHOOK_AUTH_HEADER = "Authorization";
104
+ export interface WebhookAuth {
105
+ /**
106
+ * Header the caller must send the credential in. Omitted when it is the default
107
+ * `Authorization`, whose value must be `Bearer <token>`; any other header takes the raw value.
108
+ */
109
+ headerName?: string;
110
+ /**
111
+ * Key of the team variable holding the expected value. Only the name travels, never the
112
+ * value, and the variable does not need to exist yet.
113
+ */
114
+ variableKey: string;
115
+ }
101
116
  export interface ProcessWebhook {
102
117
  enabled: boolean;
103
- username?: string;
104
- password?: string;
118
+ /** Omitted when the webhook is public, so an unprotected webhook stays minimal on disk. */
119
+ authMode?: WebhookAuthMode;
120
+ /** The webhook's own configuration, only meaningful with the CUSTOM mode. */
121
+ auth?: WebhookAuth;
105
122
  }
106
123
  export type AppRole = 'INSTALL' | 'SETTINGS' | 'USER_FACING_FORM' | 'UNINSTALL' | 'NONE';
124
+ export type FormAuthMode = 'NONE' | 'FACTORIAL';
107
125
  export interface ProcessForm {
108
126
  enabled: boolean;
109
127
  appRole?: AppRole;
128
+ authMode?: FormAuthMode;
110
129
  }
111
130
  export interface ProcessMetadataFields {
112
131
  name: string;
@@ -186,7 +205,7 @@ export declare class Variable extends Resource {
186
205
  key: string;
187
206
  value: string;
188
207
  overriddenLocally?: boolean;
189
- constructor(id: string | undefined, key: string, value: string, isSensitive: boolean);
208
+ constructor(id: string | undefined, key: string, value: string, isSensitive: boolean, resolvingTeamSlug?: string);
190
209
  static validate(variable: Variable): Variable;
191
210
  get contentHash(): string;
192
211
  }
@@ -202,13 +221,50 @@ export interface ErrorHandlerConfig {
202
221
  processSlug: string;
203
222
  tag?: string | null;
204
223
  }
224
+ export interface TeamVersionManifestEntry {
225
+ resourceType: 'MODULE' | 'PROCESS';
226
+ resourceId: string;
227
+ slug: string;
228
+ outcome: 'CREATED' | 'FAILED' | 'SKIPPED_EXISTS' | 'SKIPPED_NO_TAG' | 'UPDATED';
229
+ error?: string | null;
230
+ }
231
+ export interface TeamVersion {
232
+ id: string;
233
+ tag: string;
234
+ comment?: string | null;
235
+ manifest: TeamVersionManifestEntry[];
236
+ createdAt: string;
237
+ createdBy?: {
238
+ id: string;
239
+ name: string;
240
+ } | null;
241
+ }
242
+ export interface TeamVersionAlias {
243
+ id: string;
244
+ name: string;
245
+ tag: string;
246
+ createdAt: string;
247
+ updatedAt?: string | null;
248
+ }
249
+ export interface TeamVersionFileEntry {
250
+ tag: string;
251
+ comment?: string;
252
+ createdAt: string;
253
+ }
254
+ export interface TeamAliasFileEntry {
255
+ name: string;
256
+ tag: string;
257
+ }
205
258
  export declare class Team extends Resource {
206
259
  id: string | undefined;
207
260
  slug: string;
208
261
  zoneId?: string;
209
262
  errorHandlerConfig?: ErrorHandlerConfig;
210
263
  parentTeamSlugs: string[];
211
- constructor(slug: string, parentTeamSlugs: string[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig);
264
+ webhookAuth?: WebhookAuth;
265
+ versions?: TeamVersionFileEntry[];
266
+ aliases?: TeamAliasFileEntry[];
267
+ constructor(slug: string, parentTeamSlugs: string[], zoneId?: string, errorHandlerConfig?: ErrorHandlerConfig, webhookAuth?: WebhookAuth);
212
268
  get contentHash(): string;
213
269
  }
214
270
  export interface RawErrorHandlerConfig {
@@ -220,11 +276,16 @@ export interface RawTeamSettings {
220
276
  zoneId?: string;
221
277
  errorHandlerConfig?: RawErrorHandlerConfig | null;
222
278
  parentTeamSlugs: string[];
279
+ webhookAuth?: WebhookAuth | null;
223
280
  }
224
281
  export interface TeamUpdateInput {
225
282
  zoneId?: string;
226
283
  parentTeamSlugs?: string[];
227
284
  executionErrorHandlerConfig?: RawErrorHandlerConfig | null;
285
+ webhookAuth?: {
286
+ headerName?: string | null;
287
+ variableKey?: string | null;
288
+ } | null;
228
289
  }
229
290
  export type ResourceMetadata = {
230
291
  [key: string]: any;
@@ -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.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.programmingLanguageDisplayName = exports.ResourceStatus = exports.OnConflictResolution = exports.Resource = void 0;
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.ScriptLibrary = exports.VersionedProcess = exports.Process = exports.DEFAULT_WEBHOOK_AUTH_HEADER = exports.programmingLanguageDisplayName = 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");
@@ -37,6 +37,8 @@ const programmingLanguageDisplayName = (programmingLanguage) => {
37
37
  }
38
38
  };
39
39
  exports.programmingLanguageDisplayName = programmingLanguageDisplayName;
40
+ // The header assumed when none is configured; its value carries a Bearer scheme.
41
+ exports.DEFAULT_WEBHOOK_AUTH_HEADER = 'Authorization';
40
42
  class Process extends Resource {
41
43
  constructor(id, slug, script, readme, manifest, settings, metadata, resolvingTeamSlug) {
42
44
  super();
@@ -116,13 +118,14 @@ class VersionedScriptLibrary extends Resource {
116
118
  }
117
119
  exports.VersionedScriptLibrary = VersionedScriptLibrary;
118
120
  class Variable extends Resource {
119
- constructor(id, key, value, isSensitive) {
121
+ constructor(id, key, value, isSensitive, resolvingTeamSlug) {
120
122
  super();
121
123
  this.id = id;
122
124
  this.slug = key;
123
125
  this.key = key;
124
126
  this.value = value;
125
127
  this.isSensitive = isSensitive;
128
+ this.resolvingTeamSlug = resolvingTeamSlug;
126
129
  }
127
130
  static validate(variable) {
128
131
  if (!Variable.ENV_KEY_REGEX.test(variable.key)) {
@@ -163,19 +166,27 @@ class ProgrammingLanguageManifest extends Resource {
163
166
  }
164
167
  exports.ProgrammingLanguageManifest = ProgrammingLanguageManifest;
165
168
  class Team extends Resource {
166
- constructor(slug, parentTeamSlugs, zoneId, errorHandlerConfig) {
169
+ constructor(slug, parentTeamSlugs, zoneId, errorHandlerConfig, webhookAuth) {
167
170
  super();
168
171
  this.slug = slug;
169
172
  this.parentTeamSlugs = parentTeamSlugs;
170
173
  this.zoneId = zoneId;
171
174
  this.errorHandlerConfig = errorHandlerConfig;
175
+ this.webhookAuth = webhookAuth;
172
176
  }
173
177
  get contentHash() {
174
- return (0, canonical_hash_1.canonicalJsonHash)({
178
+ const payload = {
175
179
  zoneId: this.zoneId || null,
176
180
  errorHandlerConfig: this.errorHandlerConfig || null,
177
181
  parentTeamSlugs: [...(this.parentTeamSlugs || [])].sort(),
178
- });
182
+ };
183
+ // Inserted only when set, so every team.json that predates webhook auth keeps the
184
+ // hash already stored in .fcode and does not report a phantom conflict. A key present
185
+ // with an undefined value would not do: canonicalStringify writes that as null.
186
+ if (this.webhookAuth) {
187
+ payload.webhookAuth = this.webhookAuth;
188
+ }
189
+ return (0, canonical_hash_1.canonicalJsonHash)(payload);
179
190
  }
180
191
  }
181
192
  exports.Team = Team;
package/dist/env.d.ts CHANGED
@@ -27,7 +27,7 @@ export declare const envSchema: z.ZodObject<{
27
27
  false: "false";
28
28
  }>>;
29
29
  CLI_UX_SKIP_TTY_CHECK: z.ZodOptional<z.ZodString>;
30
- YC_COMMAND: z.ZodOptional<z.ZodString>;
30
+ FC_COMMAND: z.ZodOptional<z.ZodString>;
31
31
  }, z.core.$strip>;
32
32
  export declare const env: {
33
33
  FCODE_CLOUD_URL: string;
@@ -38,6 +38,6 @@ export declare const env: {
38
38
  FCODE_DISABLE_UPDATE_NOTIFIER?: "true" | "false" | undefined;
39
39
  SILENT_MODE?: "true" | "false" | undefined;
40
40
  CLI_UX_SKIP_TTY_CHECK?: string | undefined;
41
- YC_COMMAND?: string | undefined;
41
+ FC_COMMAND?: string | undefined;
42
42
  };
43
43
  export default env;
package/dist/env.js CHANGED
@@ -25,7 +25,7 @@ exports.envSchema = zod_1.z.object({
25
25
  SILENT_MODE: zod_1.z.enum(['true', 'false']).optional(),
26
26
  CLI_UX_SKIP_TTY_CHECK: zod_1.z.string().min(1).optional(),
27
27
  // --- Debug tooling ---
28
- YC_COMMAND: zod_1.z.string().min(1).optional(),
28
+ FC_COMMAND: zod_1.z.string().min(1).optional(),
29
29
  });
30
30
  /** Treat empty strings (e.g. an unset shell/dotenv var rendered as "") as missing. */
31
31
  const raw = Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== ''));
@@ -1,7 +1,7 @@
1
- import { RawTeamSettings, Team } from '../core/types';
1
+ import { RawTeamSettings, Team, TeamVersion, TeamVersionAlias } from '../core/types';
2
2
  import WorkspaceConfig from '../config/workspace';
3
3
  /**
4
- * Reads/writes the singleton team.json (timezone, error handler, inheritance) at the
4
+ * Reads/writes the singleton team.json (timezone, error handler, inheritance, webhook auth) at the
5
5
  * workspace root. Unlike collection resources this is not a ResourceRepositoryBase:
6
6
  * there is exactly one team, so there is no slug-keyed collection to manage.
7
7
  *
@@ -16,8 +16,16 @@ export default class TeamRepository {
16
16
  save(team: Team): void;
17
17
  /** Builds a Team from the cloud's raw settings, resolving the error-handler id to a slug. */
18
18
  fromRaw(raw: RawTeamSettings): Promise<Team>;
19
+ /**
20
+ * Writes the cloud's team versions and aliases into the Team as the pull-only team.json
21
+ * fields (sorted for stable git diffs). These fields never enter the content hash and are
22
+ * stripped from pushes: all writes go through the team:versions/team:aliases commands.
23
+ */
24
+ applyCloudVersioning(team: Team, versions: TeamVersion[], aliases: TeamVersionAlias[]): void;
19
25
  resolveSlugToId(processSlug: string): Promise<string>;
20
26
  resolveIdToSlug(processId: string): Promise<string>;
21
27
  private parseErrorHandler;
28
+ /** A configuration without a variable key is meaningless, so it is read as absent. */
29
+ private parseWebhookAuth;
22
30
  private toFile;
23
31
  }
@@ -6,7 +6,7 @@ const workspace_1 = require("../config/workspace");
6
6
  const file_utils_1 = require("../utils/file-utils");
7
7
  const TEAM_FILENAME = 'team.json';
8
8
  /**
9
- * Reads/writes the singleton team.json (timezone, error handler, inheritance) at the
9
+ * Reads/writes the singleton team.json (timezone, error handler, inheritance, webhook auth) at the
10
10
  * workspace root. Unlike collection resources this is not a ResourceRepositoryBase:
11
11
  * there is exactly one team, so there is no slug-keyed collection to manage.
12
12
  *
@@ -29,7 +29,15 @@ class TeamRepository {
29
29
  }
30
30
  catch { }
31
31
  const parentTeamSlugs = Array.isArray(raw.parentTeamSlugs) ? raw.parentTeamSlugs : [];
32
- return new types_1.Team(slug, parentTeamSlugs, raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig));
32
+ const team = new types_1.Team(slug, parentTeamSlugs, raw.zoneId, this.parseErrorHandler(raw.errorHandlerConfig), this.parseWebhookAuth(raw.webhookAuth));
33
+ // Parsed so a find()+save() round-trip does not drop the pull-only fields.
34
+ if (Array.isArray(raw.versions)) {
35
+ team.versions = raw.versions;
36
+ }
37
+ if (Array.isArray(raw.aliases)) {
38
+ team.aliases = raw.aliases;
39
+ }
40
+ return team;
33
41
  }
34
42
  save(team) {
35
43
  (0, file_utils_1.outputFileSyncIfChanged)(TEAM_FILENAME, JSON.stringify(this.toFile(team), null, 2));
@@ -43,7 +51,26 @@ class TeamRepository {
43
51
  tag: raw.errorHandlerConfig.tag ?? null,
44
52
  };
45
53
  }
46
- return new types_1.Team(raw.slug, raw.parentTeamSlugs ?? [], raw.zoneId, errorHandlerConfig);
54
+ return new types_1.Team(raw.slug, raw.parentTeamSlugs ?? [], raw.zoneId, errorHandlerConfig, this.parseWebhookAuth(raw.webhookAuth));
55
+ }
56
+ /**
57
+ * Writes the cloud's team versions and aliases into the Team as the pull-only team.json
58
+ * fields (sorted for stable git diffs). These fields never enter the content hash and are
59
+ * stripped from pushes: all writes go through the team:versions/team:aliases commands.
60
+ */
61
+ applyCloudVersioning(team, versions, aliases) {
62
+ team.versions = versions
63
+ .map((version) => {
64
+ const entry = { tag: version.tag, createdAt: version.createdAt };
65
+ if (version.comment) {
66
+ entry.comment = version.comment;
67
+ }
68
+ return entry;
69
+ })
70
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
71
+ team.aliases = aliases
72
+ .map((alias) => ({ name: alias.name, tag: alias.tag }))
73
+ .sort((a, b) => a.name.localeCompare(b.name));
47
74
  }
48
75
  async resolveSlugToId(processSlug) {
49
76
  const { processes } = await this.workspaceConfig.getActiveTeam();
@@ -60,6 +87,18 @@ class TeamRepository {
60
87
  }
61
88
  return { processSlug: raw.processSlug, tag: raw.tag ?? null };
62
89
  }
90
+ /** A configuration without a variable key is meaningless, so it is read as absent. */
91
+ parseWebhookAuth(raw) {
92
+ if (!raw || typeof raw.variableKey !== 'string' || raw.variableKey.length === 0) {
93
+ return undefined;
94
+ }
95
+ const webhookAuth = { variableKey: raw.variableKey };
96
+ // The default header is left out so a pull writes the minimal form.
97
+ if (typeof raw.headerName === 'string' && raw.headerName.length > 0 && raw.headerName !== types_1.DEFAULT_WEBHOOK_AUTH_HEADER) {
98
+ webhookAuth.headerName = raw.headerName;
99
+ }
100
+ return webhookAuth;
101
+ }
63
102
  toFile(team) {
64
103
  const file = { parentTeamSlugs: team.parentTeamSlugs ?? [] };
65
104
  if (team.zoneId !== undefined) {
@@ -68,6 +107,17 @@ class TeamRepository {
68
107
  if (team.errorHandlerConfig !== undefined) {
69
108
  file.errorHandlerConfig = team.errorHandlerConfig;
70
109
  }
110
+ // Last, so adding webhook auth to an existing team.json is a purely additive diff.
111
+ if (team.webhookAuth !== undefined) {
112
+ file.webhookAuth = team.webhookAuth;
113
+ }
114
+ // Pull-only global versioning fields, appended after the pushable settings.
115
+ if (team.versions !== undefined) {
116
+ file.versions = team.versions;
117
+ }
118
+ if (team.aliases !== undefined) {
119
+ file.aliases = team.aliases;
120
+ }
71
121
  return file;
72
122
  }
73
123
  }
@@ -4,16 +4,40 @@ export declare const DEFAULT_SENSITIVE_VALUE = "********";
4
4
  declare class VariableRepository extends ResourceRepositoryBase<Variable> {
5
5
  private filename;
6
6
  private localFilename;
7
+ private inheritedFilename;
7
8
  private filenameInitPromise;
8
9
  constructor();
9
10
  private ensureFilenamesInitialized;
10
11
  findAll(): Promise<Variable[]>;
12
+ /**
13
+ * Values as a local run sees them. Precedence, lowest first: variables inherited from
14
+ * parent workspaces, this workspace's own variables, and the local overrides file — the
15
+ * same order the platform applies, where a workspace's own value wins over an inherited one.
16
+ * An empty value is not an override but an unset one: the platform rejects blank values, and
17
+ * a variable emptied in variables.env is a removal (see `mapResourceStatus`), so an empty
18
+ * entry falls through to the layer below rather than blanking it.
19
+ */
11
20
  findAllForExecution(): Promise<Variable[]>;
21
+ /**
22
+ * An inherited key the workspace has given a value of its own, overriding it. An empty entry
23
+ * does not count: the platform has no empty values, so it reads as "not set" here too.
24
+ */
25
+ private overridesInherited;
12
26
  find(slug: string): Promise<Variable>;
27
+ /**
28
+ * Keys declared in the workspace's own variables file that its metadata does not claim yet.
29
+ * A key the workspace inherits counts as one of those: the metadata still points at the
30
+ * parent's variable, and adding it is what turns the entry into this workspace's own.
31
+ */
13
32
  findSlugsNotAdded(): Promise<Array<string>>;
14
33
  protected getResourcePath(_resource: Variable): string;
15
34
  protected removeFiles(variable: Variable): Promise<void>;
16
35
  protected resourceToMetadata(variable: Variable): ResourceMetadata;
36
+ /**
37
+ * A variable owned by this workspace goes to the committed variables file; one inherited
38
+ * from a parent workspace goes to the gitignored inherited file, so a local run can use it
39
+ * without the child workspace's git history carrying a copy of its parent's values.
40
+ */
17
41
  protected saveFiles(variable: Variable): Promise<void>;
18
42
  metadataFromFiles(slug: string, opts: AddResourceOptions): Promise<ResourceMetadata>;
19
43
  private readVariablesMeta;
@@ -20,6 +20,7 @@ class VariableRepository extends resource_1.default {
20
20
  });
21
21
  this.filename = null;
22
22
  this.localFilename = null;
23
+ this.inheritedFilename = null;
23
24
  this.filenameInitPromise = null;
24
25
  }
25
26
  async ensureFilenamesInitialized() {
@@ -30,40 +31,67 @@ class VariableRepository extends resource_1.default {
30
31
  const globalConfig = new global_1.default();
31
32
  this.filename = await globalConfig.getSetting(settings_1.SettingKeys.VARIABLES_FILE_NAME);
32
33
  this.localFilename = await globalConfig.getSetting(settings_1.SettingKeys.LOCAL_VARIABLES_FILE_NAME);
34
+ this.inheritedFilename = await globalConfig.getSetting(settings_1.SettingKeys.INHERITED_VARIABLES_FILE_NAME);
33
35
  })();
34
36
  return this.filenameInitPromise;
35
37
  }
36
38
  async findAll() {
37
39
  await this.ensureFilenamesInitialized();
38
- const rawVariables = this.readVariablesFromFile();
39
- const localOverrideKeys = new Set(Object.keys(this.readVariablesFromFile(true)));
40
+ const rawVariables = this.readVariablesFromFile('own');
41
+ const inheritedRawVariables = this.readVariablesFromFile('inherited');
42
+ const localOverrideKeys = new Set(Object.keys(this.readVariablesFromFile('local')));
40
43
  const meta = this.readVariablesMeta();
41
44
  const variables = [];
42
45
  const { variables: localVariables } = await this.workspaceConfig.getActiveTeam();
43
46
  for (const localVariableKey of Object.keys(localVariables)) {
44
47
  const localVariable = localVariables[localVariableKey];
45
48
  const isSensitive = meta[localVariable.key]?.isSensitive ?? false;
46
- const variable = types_1.Variable.validate(new types_1.Variable(localVariable.id, localVariable.key, rawVariables[localVariable.key], isSensitive));
49
+ // Writing an inherited key into variables.env is how you override it, so from that
50
+ // moment the variable is this workspace's: it drops the parent's id and inherited
51
+ // marker, which is what makes `push` create it here instead of skipping it.
52
+ const overridesInherited = this.overridesInherited(localVariable, rawVariables);
53
+ const resolvingTeamSlug = overridesInherited ? undefined : localVariable.resolvingTeamSlug;
54
+ const id = overridesInherited ? undefined : localVariable.id;
55
+ const rawValue = resolvingTeamSlug ? inheritedRawVariables[localVariable.key] : rawVariables[localVariable.key];
56
+ const variable = types_1.Variable.validate(new types_1.Variable(id, localVariable.key, rawValue, isSensitive, resolvingTeamSlug));
47
57
  variable.overriddenLocally = localOverrideKeys.has(localVariable.key);
48
58
  variables.push(variable);
49
59
  }
50
60
  return variables;
51
61
  }
62
+ /**
63
+ * Values as a local run sees them. Precedence, lowest first: variables inherited from
64
+ * parent workspaces, this workspace's own variables, and the local overrides file — the
65
+ * same order the platform applies, where a workspace's own value wins over an inherited one.
66
+ * An empty value is not an override but an unset one: the platform rejects blank values, and
67
+ * a variable emptied in variables.env is a removal (see `mapResourceStatus`), so an empty
68
+ * entry falls through to the layer below rather than blanking it.
69
+ */
52
70
  async findAllForExecution() {
53
71
  await this.ensureFilenamesInitialized();
54
- const rawVariables = this.readVariablesFromFile();
55
- const localRawVariables = this.readVariablesFromFile(true);
72
+ const rawVariables = this.readVariablesFromFile('own');
73
+ const inheritedRawVariables = this.readVariablesFromFile('inherited');
74
+ const localRawVariables = this.readVariablesFromFile('local');
56
75
  const meta = this.readVariablesMeta();
57
76
  const variables = [];
58
77
  const { variables: localVariables } = await this.workspaceConfig.getActiveTeam();
59
78
  for (const localVariableKey of Object.keys(localVariables)) {
60
79
  const localVariable = localVariables[localVariableKey];
61
- const value = localRawVariables[localVariable.key] || rawVariables[localVariable.key];
62
- const isSensitive = meta[localVariable.key]?.isSensitive ?? false;
63
- variables.push(types_1.Variable.validate(new types_1.Variable(localVariable.id, localVariable.key, value, isSensitive)));
80
+ const key = localVariable.key;
81
+ const isSensitive = meta[key]?.isSensitive ?? false;
82
+ const value = localRawVariables[key] || rawVariables[key] || inheritedRawVariables[key];
83
+ const resolvingTeamSlug = this.overridesInherited(localVariable, rawVariables) ? undefined : localVariable.resolvingTeamSlug;
84
+ variables.push(types_1.Variable.validate(new types_1.Variable(localVariable.id, key, value, isSensitive, resolvingTeamSlug)));
64
85
  }
65
86
  return variables;
66
87
  }
88
+ /**
89
+ * An inherited key the workspace has given a value of its own, overriding it. An empty entry
90
+ * does not count: the platform has no empty values, so it reads as "not set" here too.
91
+ */
92
+ overridesInherited(metadata, ownRawVariables) {
93
+ return Boolean(metadata.resolvingTeamSlug) && Boolean(ownRawVariables[metadata.key]);
94
+ }
67
95
  async find(slug) {
68
96
  let variables = await this.findAll();
69
97
  variables = variables.filter((v) => v.key === slug);
@@ -72,12 +100,19 @@ class VariableRepository extends resource_1.default {
72
100
  }
73
101
  throw new errors_1.LocalVariableNotFound(new Error('Variable not found on team metadata'), slug);
74
102
  }
103
+ /**
104
+ * Keys declared in the workspace's own variables file that its metadata does not claim yet.
105
+ * A key the workspace inherits counts as one of those: the metadata still points at the
106
+ * parent's variable, and adding it is what turns the entry into this workspace's own.
107
+ */
75
108
  async findSlugsNotAdded() {
76
109
  await this.ensureFilenamesInitialized();
110
+ const ownRawVariables = this.readVariablesFromFile('own');
77
111
  const { variables: localVariables } = await this.workspaceConfig.getActiveTeam();
78
112
  const slugsNotAdded = [];
79
- for (const [key, _value] of Object.entries(this.readVariablesFromFile())) {
80
- if (!localVariables[key]) {
113
+ for (const key of Object.keys(ownRawVariables)) {
114
+ const metadata = localVariables[key];
115
+ if (!metadata || this.overridesInherited(metadata, ownRawVariables)) {
81
116
  slugsNotAdded.push(key);
82
117
  }
83
118
  }
@@ -88,9 +123,10 @@ class VariableRepository extends resource_1.default {
88
123
  }
89
124
  async removeFiles(variable) {
90
125
  await this.ensureFilenamesInitialized();
91
- let rawVariables = this.readVariablesFromFile();
126
+ const file = variable.resolvingTeamSlug ? 'inherited' : 'own';
127
+ const rawVariables = this.readVariablesFromFile(file);
92
128
  delete rawVariables[variable.key];
93
- this.writeRawVariables(rawVariables);
129
+ this.writeRawVariables(rawVariables, file);
94
130
  const meta = this.readVariablesMeta();
95
131
  delete meta[variable.key];
96
132
  this.writeVariablesMeta(meta);
@@ -100,13 +136,20 @@ class VariableRepository extends resource_1.default {
100
136
  id: variable.id,
101
137
  slug: variable.slug,
102
138
  key: variable.key,
139
+ ...(variable.resolvingTeamSlug && { resolvingTeamSlug: variable.resolvingTeamSlug }),
103
140
  };
104
141
  }
142
+ /**
143
+ * A variable owned by this workspace goes to the committed variables file; one inherited
144
+ * from a parent workspace goes to the gitignored inherited file, so a local run can use it
145
+ * without the child workspace's git history carrying a copy of its parent's values.
146
+ */
105
147
  async saveFiles(variable) {
106
148
  await this.ensureFilenamesInitialized();
107
- let rawVariables = this.readVariablesFromFile();
108
- rawVariables = Object.assign(rawVariables, { [variable.key]: this.valueToSave(variable) });
109
- this.writeRawVariables(rawVariables);
149
+ const file = variable.resolvingTeamSlug ? 'inherited' : 'own';
150
+ const rawVariables = this.readVariablesFromFile(file);
151
+ rawVariables[variable.key] = this.valueToSave(variable);
152
+ this.writeRawVariables(rawVariables, file);
110
153
  const meta = this.readVariablesMeta();
111
154
  meta[variable.key] = { isSensitive: variable.isSensitive };
112
155
  this.writeVariablesMeta(meta);
@@ -139,15 +182,20 @@ class VariableRepository extends resource_1.default {
139
182
  }, {});
140
183
  (0, file_utils_1.outputFileSyncIfChanged)(VARIABLES_META_FILENAME, JSON.stringify(sorted, null, 2));
141
184
  }
142
- filePath(useLocalVersion = false) {
143
- if (!this.filename || !this.localFilename) {
185
+ filePath(file = 'own') {
186
+ if (!this.filename || !this.localFilename || !this.inheritedFilename) {
144
187
  throw new Error('Filenames not initialized. Call ensureFilenamesInitialized() first.');
145
188
  }
146
- return path.join(useLocalVersion ? this.localFilename : this.filename);
189
+ const filenames = {
190
+ own: this.filename,
191
+ local: this.localFilename,
192
+ inherited: this.inheritedFilename,
193
+ };
194
+ return path.join(filenames[file]);
147
195
  }
148
- readVariablesFromFile(useLocalVersion = false) {
196
+ readVariablesFromFile(file = 'own') {
149
197
  try {
150
- const result = (0, dotenv_1.parse)(fs.readFileSync(this.filePath(useLocalVersion), 'utf8')) || {};
198
+ const result = (0, dotenv_1.parse)(fs.readFileSync(this.filePath(file), 'utf8')) || {};
151
199
  for (const [key, value] of Object.entries(result)) {
152
200
  result[key] = (0, env_utils_1.unescapeEnvValue)(value);
153
201
  }
@@ -163,12 +211,12 @@ class VariableRepository extends resource_1.default {
163
211
  }
164
212
  return variable.value;
165
213
  }
166
- writeRawVariables(rawVariables) {
214
+ writeRawVariables(rawVariables, file = 'own') {
167
215
  const envContent = Object.entries(rawVariables)
168
216
  .sort((a, b) => a[0].localeCompare(b[0]))
169
217
  .map(([key, value]) => `${key}=${(0, env_utils_1.escapeEnvValue)(value)}`)
170
218
  .join('\n');
171
- (0, file_utils_1.outputFileSyncIfChanged)(this.filePath(), envContent);
219
+ (0, file_utils_1.outputFileSyncIfChanged)(this.filePath(file), envContent);
172
220
  }
173
221
  }
174
222
  exports.default = VariableRepository;
@@ -3,7 +3,7 @@ const path = require('node:path')
3
3
  const { execSync } = require('node:child_process')
4
4
  const os = require('node:os')
5
5
 
6
- const YC_COMMAND = process.env.YC_COMMAND ?? "fcode"
6
+ const FC_COMMAND = process.env.FC_COMMAND ?? "fcode"
7
7
 
8
8
  const file = process.argv[2]
9
9
  if (!fs.existsSync(file)) {
@@ -36,7 +36,7 @@ let dependenciesPath = ''
36
36
  try {
37
37
  version = getVersion(relativePath)
38
38
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fcode-'))
39
- const command = [YC_COMMAND, 'pre-debug', absolutePath, '-t', tempDir]
39
+ const command = [FC_COMMAND, 'pre-debug', absolutePath, '-t', tempDir]
40
40
  if (version) {
41
41
  command.push('-v')
42
42
  command.push(version)
@@ -4,7 +4,7 @@ import subprocess
4
4
  import tempfile
5
5
 
6
6
 
7
- YC_COMMAND = os.environ.get("YC_COMMAND", "fcode")
7
+ FC_COMMAND = os.environ.get("FC_COMMAND", "fcode")
8
8
  file = sys.argv[1]
9
9
  if not os.path.exists(file):
10
10
  print("File not found: " + file)
@@ -31,7 +31,7 @@ main_file_path = ""
31
31
  try:
32
32
  version = get_version(file)
33
33
  tempDir = tempfile.TemporaryDirectory()
34
- command = [YC_COMMAND, "pre-debug", file, "-t", tempDir.name]
34
+ command = [FC_COMMAND, "pre-debug", file, "-t", tempDir.name]
35
35
  if version != None:
36
36
  command.append("-v")
37
37
  command.append(version)
@@ -3,7 +3,7 @@ import json
3
3
  import subprocess
4
4
  import logging
5
5
 
6
- YC_COMMAND = os.environ.get("YC_COMMAND", "fcode")
6
+ FC_COMMAND = os.environ.get("FC_COMMAND", "fcode")
7
7
 
8
8
  metadata = {
9
9
  "execution": {"process": {"name": "", "id": ""}},
@@ -24,7 +24,7 @@ else:
24
24
  os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
25
25
  "[FCODE LIBRARY] Reading metadata from process-metadata command"
26
26
  )
27
- command = [YC_COMMAND, "process-metadata"]
27
+ command = [FC_COMMAND, "process-metadata"]
28
28
  if process_slug:
29
29
  command.append(process_slug)
30
30
  if process_slug and process_version:
@@ -6,6 +6,9 @@ declare const AuthService: {
6
6
  getUserInfo(authUrl: string, token: Token): Promise<UserInfo>;
7
7
  globalConfig: GlobalConfig;
8
8
  isAuthenticated(url: string): Promise<boolean>;
9
+ isTokenExpiring(token: Token): boolean;
10
+ refreshToken(authUrl: string, refreshToken: string): Promise<Token>;
11
+ tryRefresh(url: string, { authUrl, token, userInfo }: AuthInfo): Promise<boolean>;
9
12
  login({ url, authUrl, email, password, clientId, clientSecret, accessToken }: Credentials): Promise<AuthInfo>;
10
13
  logout(url: string): Promise<void>;
11
14
  };