@project-ajax/sdk 0.0.54 → 0.0.55

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.
@@ -1 +1 @@
1
- {"version":3,"file":"auth.impl.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/auth.impl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,eAAO,MAAM,KAAK,+GA+BhB,CAAC;AAEH,eAAO,MAAM,IAAI,mFAEf,CAAC;AAEH,eAAO,MAAM,MAAM,mFAEjB,CAAC"}
1
+ {"version":3,"file":"auth.impl.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/auth.impl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,eAAO,MAAM,KAAK,+GAiChB,CAAC;AAEH,eAAO,MAAM,IAAI,mFAEf,CAAC;AAEH,eAAO,MAAM,MAAM,mFAIjB,CAAC"}
@@ -19,16 +19,20 @@ ${url}
19
19
  }
20
20
  return;
21
21
  }
22
- await this.config.setEnvironment(environment);
23
- await this.config.setToken(token);
24
- await this.config.setWorkerId(null);
22
+ await this.config.update({
23
+ environment,
24
+ token,
25
+ workerId: null
26
+ });
25
27
  this.writer.writeErr("Successfully logged in!");
26
28
  });
27
29
  const show = buildHandler(function() {
28
30
  this.writer.writeOut(`${this.config.token ?? ""}`);
29
31
  });
30
32
  const logout = buildHandler(async function() {
31
- await this.config.setToken(null);
33
+ await this.config.update({
34
+ token: null
35
+ });
32
36
  });
33
37
  export {
34
38
  login,
@@ -53,7 +53,7 @@ const deploy = buildAuthedHandler(async function(flags) {
53
53
  }
54
54
  if (Result.isSuccess(result)) {
55
55
  const { workerId: workerId2 } = Result.unwrap(result);
56
- await this.config.setWorkerId(workerId2);
56
+ await this.config.update({ workerId: workerId2 });
57
57
  this.writer.writeErr("\u2713 Successfully deployed worker");
58
58
  } else {
59
59
  this.writer.writeErr("\u2717 Failed to deploy worker");
@@ -1,19 +1,16 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { Config, type Environment } from "../../config.js";
4
+ import { Config, type ConfigMap } from "../../config.js";
5
5
  import type { GlobalFlags } from "../../flags.js";
6
6
  import { Writer } from "../../writer.js";
7
- export type ConfigFileContents = {
8
- token: string | null;
9
- workerId: string | null;
10
- environment: Environment;
11
- baseUrl: string;
12
- };
13
7
  export declare const tmpDirectories: string[];
14
8
  export declare const baseFlags: GlobalFlags;
15
- export declare function createConfigFile(contents: ConfigFileContents): Promise<string>;
16
- export declare function createTestConfig(configFileContents: ConfigFileContents): Promise<Config>;
9
+ export declare function createAndLoadConfig({ configFile, env, flags, }: {
10
+ configFile: Partial<ConfigMap>;
11
+ env?: Partial<NodeJS.ProcessEnv>;
12
+ flags?: Partial<GlobalFlags>;
13
+ }): Promise<[config: Config, path: string]>;
17
14
  export declare function createBaseContext(): {
18
15
  writer: Writer;
19
16
  process: NodeJS.Process;
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/utils/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,MAAM,MAAM,kBAAkB,GAAG;IAChC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,MAAM,EAAO,CAAC;AAE3C,eAAO,MAAM,SAAS,EAAE,WAEvB,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,kBAAkB,mBAOlE;AAED,wBAAsB,gBAAgB,CACrC,kBAAkB,EAAE,kBAAkB,GACpC,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED,wBAAgB,iBAAiB;;;;;;EAQhC;AAED,wBAAsB,qBAAqB,kBAO1C"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/utils/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,eAAO,MAAM,cAAc,EAAE,MAAM,EAAO,CAAC;AAE3C,eAAO,MAAM,SAAS,EAAE,WAEvB,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,EACzC,UAAU,EACV,GAAG,EACH,KAAK,GACL,EAAE;IACF,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/B,GAAG,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC7B,GAAG,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAa1C;AAED,wBAAgB,iBAAiB;;;;;;EAQhC;AAED,wBAAsB,qBAAqB,kBAO1C"}
@@ -9,20 +9,21 @@ const tmpDirectories = [];
9
9
  const baseFlags = {
10
10
  debug: false
11
11
  };
12
- async function createConfigFile(contents) {
13
- const dir = await mkdtemp(path.join(tmpdir(), "cmd-test-"));
14
- tmpDirectories.push(dir);
15
- const configFilePath = path.join(dir, "config.json");
16
- await writeFile(configFilePath, JSON.stringify(contents, null, 2), "utf-8");
17
- return configFilePath;
18
- }
19
- async function createTestConfig(configFileContents) {
20
- const configFilePath = await createConfigFile(configFileContents);
21
- return Config.load({
12
+ async function createAndLoadConfig({
13
+ configFile,
14
+ env,
15
+ flags
16
+ }) {
17
+ const configFilePath = await createConfigFile(configFile);
18
+ const map = await Config.load({
22
19
  configFilePath,
23
- processEnv: {},
24
- flags: { debug: false }
20
+ processEnv: env ?? {},
21
+ flags: {
22
+ ...baseFlags,
23
+ ...flags ?? {}
24
+ }
25
25
  });
26
+ return [map, configFilePath];
26
27
  }
27
28
  function createBaseContext() {
28
29
  return {
@@ -41,11 +42,17 @@ async function cleanupTmpDirectories() {
41
42
  }
42
43
  }
43
44
  }
45
+ async function createConfigFile(contents) {
46
+ const dir = await mkdtemp(path.join(tmpdir(), "cmd-test-"));
47
+ tmpDirectories.push(dir);
48
+ const configFilePath = path.join(dir, "config.json");
49
+ await writeFile(configFilePath, JSON.stringify(contents, null, 2), "utf-8");
50
+ return configFilePath;
51
+ }
44
52
  export {
45
53
  baseFlags,
46
54
  cleanupTmpDirectories,
55
+ createAndLoadConfig,
47
56
  createBaseContext,
48
- createConfigFile,
49
- createTestConfig,
50
57
  tmpDirectories
51
58
  };
@@ -1,7 +1,7 @@
1
1
  import type { GlobalFlags } from "./flags.js";
2
2
  export declare const Environments: readonly ["local", "staging", "dev", "prod"];
3
3
  export type Environment = (typeof Environments)[number];
4
- interface ConfigMap {
4
+ export interface ConfigMap {
5
5
  environment: Environment;
6
6
  baseUrl: string;
7
7
  token: string | null;
@@ -26,21 +26,29 @@ export declare class TokenNotSetError extends Error {
26
26
  export declare class Config {
27
27
  #private;
28
28
  constructor(opts: {
29
+ configMap: ConfigMap;
29
30
  configFilePath: string;
30
- configFile: ConfigMap;
31
31
  });
32
32
  get baseUrl(): string;
33
33
  get token(): string | null;
34
34
  get environment(): "local" | "staging" | "dev" | "prod";
35
35
  get workerId(): string | null;
36
- setEnvironment(environment: Environment): Promise<void>;
37
- setToken(token: string | null): Promise<void>;
38
- setWorkerId(workerId: string | null): Promise<void>;
39
36
  get tokenInfo(): {
40
37
  token: string;
41
38
  spaceId: string;
42
39
  cellId: string;
43
40
  };
41
+ /**
42
+ * Update the config with a partial config map.
43
+ *
44
+ * This will write only the updated keys in the config file on disk. Not all
45
+ * keys are written, since some current keys in the Config object may have
46
+ * come from e.g. environment variables, rather than the original config
47
+ * file.
48
+ *
49
+ * @param config The config update.
50
+ */
51
+ update(config: Partial<ConfigMap>): Promise<void>;
44
52
  static load(opts: {
45
53
  configFilePath: string;
46
54
  processEnv: NodeJS.ProcessEnv;
@@ -52,5 +60,4 @@ export declare function extractPayloadFromToken(token: string): {
52
60
  userId: string;
53
61
  cellId: string;
54
62
  };
55
- export {};
56
63
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,YAAY,8CAA+C,CAAC;AACzE,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,UAAU,SAAS;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAM1D;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAEzC,OAAO,GAAE,MAA6D;CAKvE;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,MAAM;;gBAIN,IAAI,EAAE;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,SAAS,CAAC;KACtB;IAOD,IAAI,OAAO,WAEV;IAED,IAAI,KAAK,kBAER;IAED,IAAI,WAAW,yCAEd;IAED,IAAI,QAAQ,kBAEX;IAIK,cAAc,CAAC,WAAW,EAAE,WAAW;IAMvC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK7B,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAazC,IAAI,SAAS,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAQlE;WAEY,IAAI,CAAC,IAAI,EAAE;QACvB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;QAC9B,KAAK,EAAE,WAAW,CAAC;KACnB;CAkED;AAQD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf,CAoCA"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,YAAY,8CAA+C,CAAC;AACzE,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD,MAAM,WAAW,SAAS;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAM1D;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAEzC,OAAO,GAAE,MAA6D;CAKvE;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,MAAM;;gBAIN,IAAI,EAAE;QACjB,SAAS,EAAE,SAAS,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;KACvB;IAOD,IAAI,OAAO,WAEV;IAED,IAAI,KAAK,kBAER;IAED,IAAI,WAAW,yCAEd;IAED,IAAI,QAAQ,kBAEX;IAED,IAAI,SAAS,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAQlE;IAED;;;;;;;;;OASG;IACG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC;WAc1B,IAAI,CAAC,IAAI,EAAE;QACvB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;QAC9B,KAAK,EAAE,WAAW,CAAC;KACnB;CA+FD;AAQD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf,CAoCA"}
@@ -17,8 +17,8 @@ class Config {
17
17
  #configMap;
18
18
  #configFilePath;
19
19
  constructor(opts) {
20
+ this.#configMap = opts.configMap;
20
21
  this.#configFilePath = opts.configFilePath;
21
- this.#configMap = opts.configFile;
22
22
  }
23
23
  // Getters read from the environment variables, and then the config file.
24
24
  get baseUrl() {
@@ -33,27 +33,6 @@ class Config {
33
33
  get workerId() {
34
34
  return this.#configMap.workerId;
35
35
  }
36
- // Setters update the config file, and then write it to disk.
37
- async setEnvironment(environment) {
38
- this.#configMap.environment = environment;
39
- this.#configMap.baseUrl = baseUrl(environment);
40
- await this.#writeConfigFile();
41
- }
42
- async setToken(token) {
43
- this.#configMap.token = token;
44
- await this.#writeConfigFile();
45
- }
46
- async setWorkerId(workerId) {
47
- this.#configMap.workerId = workerId;
48
- await this.#writeConfigFile();
49
- }
50
- async #writeConfigFile() {
51
- await fs.promises.writeFile(
52
- this.#configFilePath,
53
- JSON.stringify(this.#configMap, null, 2),
54
- "utf-8"
55
- );
56
- }
57
36
  get tokenInfo() {
58
37
  const token = this.token;
59
38
  if (!token) {
@@ -62,39 +41,85 @@ class Config {
62
41
  const { spaceId, cellId } = extractPayloadFromToken(token);
63
42
  return { token, spaceId, cellId };
64
43
  }
44
+ /**
45
+ * Update the config with a partial config map.
46
+ *
47
+ * This will write only the updated keys in the config file on disk. Not all
48
+ * keys are written, since some current keys in the Config object may have
49
+ * come from e.g. environment variables, rather than the original config
50
+ * file.
51
+ *
52
+ * @param config The config update.
53
+ */
54
+ async update(config) {
55
+ Object.assign(this.#configMap, config);
56
+ const currentConfigFile = await Config.#readConfigFile(
57
+ this.#configFilePath
58
+ );
59
+ Object.assign(currentConfigFile, config);
60
+ await fs.promises.writeFile(
61
+ this.#configFilePath,
62
+ JSON.stringify(currentConfigFile, null, 2),
63
+ "utf-8"
64
+ );
65
+ }
65
66
  static async load(opts) {
66
67
  const absConfigFilePath = path.resolve(process.cwd(), opts.configFilePath);
67
- const configFile = await Config.#readConfigFile(absConfigFilePath);
68
+ const partialConfig = await Config.#readConfigFile(absConfigFilePath);
68
69
  if (opts.processEnv.WORKERS_TOKEN) {
69
- configFile.token = opts.processEnv.WORKERS_TOKEN;
70
+ partialConfig.token = opts.processEnv.WORKERS_TOKEN;
70
71
  }
71
72
  if (opts.processEnv.WORKERS_ENVIRONMENT) {
72
- configFile.environment = parseEnvironment(
73
+ partialConfig.environment = parseEnvironment(
73
74
  opts.processEnv.WORKERS_ENVIRONMENT
74
75
  );
75
76
  }
76
77
  if (opts.processEnv.WORKERS_WORKER_ID) {
77
- configFile.workerId = opts.processEnv.WORKERS_WORKER_ID;
78
+ partialConfig.workerId = opts.processEnv.WORKERS_WORKER_ID;
78
79
  }
79
80
  if (opts.processEnv.WORKERS_BASE_URL) {
80
- configFile.baseUrl = opts.processEnv.WORKERS_BASE_URL;
81
+ partialConfig.baseUrl = opts.processEnv.WORKERS_BASE_URL;
81
82
  }
82
83
  if (opts.flags.token) {
83
- configFile.token = opts.flags.token;
84
+ partialConfig.token = opts.flags.token;
84
85
  }
85
86
  if (opts.flags.env) {
86
- configFile.environment = parseEnvironment(opts.flags.env);
87
+ partialConfig.environment = parseEnvironment(opts.flags.env);
87
88
  }
88
89
  if (opts.flags["base-url"]) {
89
- configFile.baseUrl = opts.flags["base-url"];
90
+ partialConfig.baseUrl = opts.flags["base-url"];
90
91
  }
91
92
  if (opts.flags["worker-id"]) {
92
- configFile.workerId = opts.flags["worker-id"];
93
+ partialConfig.workerId = opts.flags["worker-id"];
94
+ }
95
+ partialConfig.baseUrl ??= baseUrlForEnvironment(
96
+ partialConfig.environment ?? "prod"
97
+ );
98
+ const environment = partialConfig.environment;
99
+ if (!environment) {
100
+ throw new Error("Environment is required");
101
+ }
102
+ const baseUrl = partialConfig.baseUrl;
103
+ if (!baseUrl) {
104
+ throw new Error("Base URL is required");
105
+ }
106
+ const token = partialConfig.token;
107
+ if (token === void 0) {
108
+ throw new Error("Token is required");
93
109
  }
94
- configFile.baseUrl ??= baseUrl(configFile.environment ?? "prod");
110
+ const workerId = partialConfig.workerId;
111
+ if (workerId === void 0) {
112
+ throw new Error("Worker ID is required");
113
+ }
114
+ const configMap = {
115
+ environment,
116
+ baseUrl,
117
+ token,
118
+ workerId
119
+ };
95
120
  return new Config({
96
121
  configFilePath: absConfigFilePath,
97
- configFile
122
+ configMap
98
123
  });
99
124
  }
100
125
  static async #readConfigFile(configFilePath) {
@@ -111,7 +136,7 @@ class Config {
111
136
  token: null,
112
137
  workerId: null,
113
138
  environment: "prod",
114
- baseUrl: baseUrl("prod")
139
+ baseUrl: baseUrlForEnvironment("prod")
115
140
  };
116
141
  } else {
117
142
  throw error;
@@ -151,7 +176,7 @@ function extractPayloadFromToken(token) {
151
176
  throw new Error("Failed to parse token payload.");
152
177
  }
153
178
  }
154
- function baseUrl(environment) {
179
+ function baseUrlForEnvironment(environment) {
155
180
  switch (environment) {
156
181
  case "local":
157
182
  return "http://localhost:3000";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-ajax/sdk",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "description": "An SDK for building workers for the Project Ajax platform",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -11,8 +11,8 @@ import { Config } from "../config.js";
11
11
  import {
12
12
  baseFlags,
13
13
  cleanupTmpDirectories,
14
+ createAndLoadConfig,
14
15
  createBaseContext,
15
- createTestConfig,
16
16
  } from "./utils/testing.js";
17
17
 
18
18
  // Mock the openUrl module before importing the implementation
@@ -35,11 +35,13 @@ describe("login", () => {
35
35
  });
36
36
 
37
37
  it("opens browser and displays instructions when no token provided", async () => {
38
- const mockConfig = await createTestConfig({
39
- token: null,
40
- workerId: null,
41
- environment: "local",
42
- baseUrl: "http://localhost:3000",
38
+ const [mockConfig] = await createAndLoadConfig({
39
+ configFile: {
40
+ token: null,
41
+ workerId: null,
42
+ environment: "local",
43
+ baseUrl: "http://localhost:3000",
44
+ },
43
45
  });
44
46
 
45
47
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -63,11 +65,13 @@ describe("login", () => {
63
65
  });
64
66
 
65
67
  it("shows error message when browser fails to open", async () => {
66
- const mockConfig = await createTestConfig({
67
- token: null,
68
- workerId: null,
69
- environment: "local",
70
- baseUrl: "http://localhost:3000",
68
+ const [mockConfig] = await createAndLoadConfig({
69
+ configFile: {
70
+ token: null,
71
+ workerId: null,
72
+ environment: "local",
73
+ baseUrl: "http://localhost:3000",
74
+ },
71
75
  });
72
76
 
73
77
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -85,11 +89,13 @@ describe("login", () => {
85
89
  });
86
90
 
87
91
  it("saves token and clears workerId when token is provided", async () => {
88
- const mockConfig = await createTestConfig({
89
- token: null,
90
- workerId: null,
91
- environment: "local",
92
- baseUrl: "http://localhost:3000",
92
+ const [mockConfig] = await createAndLoadConfig({
93
+ configFile: {
94
+ token: null,
95
+ workerId: null,
96
+ environment: "local",
97
+ baseUrl: "http://localhost:3000",
98
+ },
93
99
  });
94
100
 
95
101
  // Spy on Config.load to return our mock config
@@ -98,28 +104,30 @@ describe("login", () => {
98
104
  const testToken =
99
105
  "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig";
100
106
 
101
- const setEnvironmentSpy = vi.spyOn(mockConfig, "setEnvironment");
102
- const setTokenSpy = vi.spyOn(mockConfig, "setToken");
103
- const setWorkerIdSpy = vi.spyOn(mockConfig, "setWorkerId");
107
+ const updateSpy = vi.spyOn(mockConfig, "update");
104
108
 
105
109
  const context = createBaseContext();
106
110
 
107
111
  await login.call(context, baseFlags, testToken);
108
112
 
109
- expect(setEnvironmentSpy).toHaveBeenCalledWith("local");
110
- expect(setTokenSpy).toHaveBeenCalledWith(testToken);
111
- expect(setWorkerIdSpy).toHaveBeenCalledWith(null);
113
+ expect(updateSpy).toHaveBeenCalledWith({
114
+ environment: "local",
115
+ token: testToken,
116
+ workerId: null,
117
+ });
112
118
  expect(stderrSpy).toHaveBeenCalledWith(
113
119
  expect.stringContaining("Successfully logged in!"),
114
120
  );
115
121
  });
116
122
 
117
123
  it("saves environment from config when logging in", async () => {
118
- const mockConfig = await createTestConfig({
119
- token: null,
120
- workerId: null,
121
- environment: "prod",
122
- baseUrl: "https://www.notion.so",
124
+ const [mockConfig] = await createAndLoadConfig({
125
+ configFile: {
126
+ token: null,
127
+ workerId: null,
128
+ environment: "prod",
129
+ baseUrl: "https://www.notion.so",
130
+ },
123
131
  });
124
132
 
125
133
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -127,13 +135,17 @@ describe("login", () => {
127
135
  const testToken =
128
136
  "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig";
129
137
 
130
- const setEnvironmentSpy = vi.spyOn(mockConfig, "setEnvironment");
138
+ const updateSpy = vi.spyOn(mockConfig, "update");
131
139
 
132
140
  const context = createBaseContext();
133
141
 
134
142
  await login.call(context, baseFlags, testToken);
135
143
 
136
- expect(setEnvironmentSpy).toHaveBeenCalledWith("prod");
144
+ expect(updateSpy).toHaveBeenCalledWith({
145
+ environment: "prod",
146
+ token: testToken,
147
+ workerId: null,
148
+ });
137
149
  });
138
150
  });
139
151
 
@@ -150,11 +162,13 @@ describe("show", () => {
150
162
  const testToken =
151
163
  "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig";
152
164
 
153
- const mockConfig = await createTestConfig({
154
- token: testToken,
155
- workerId: "worker-1",
156
- environment: "local",
157
- baseUrl: "http://localhost:3000",
165
+ const [mockConfig] = await createAndLoadConfig({
166
+ configFile: {
167
+ token: testToken,
168
+ workerId: "worker-1",
169
+ environment: "local",
170
+ baseUrl: "http://localhost:3000",
171
+ },
158
172
  });
159
173
 
160
174
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -167,11 +181,13 @@ describe("show", () => {
167
181
  });
168
182
 
169
183
  it("writes empty string to stdout when no token exists", async () => {
170
- const mockConfig = await createTestConfig({
171
- token: null,
172
- workerId: null,
173
- environment: "local",
174
- baseUrl: "http://localhost:3000",
184
+ const [mockConfig] = await createAndLoadConfig({
185
+ configFile: {
186
+ token: null,
187
+ workerId: null,
188
+ environment: "local",
189
+ baseUrl: "http://localhost:3000",
190
+ },
175
191
  });
176
192
 
177
193
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -186,21 +202,25 @@ describe("show", () => {
186
202
 
187
203
  describe("logout", () => {
188
204
  it("calls setToken with null", async () => {
189
- const mockConfig = await createTestConfig({
190
- token: "existing-token",
191
- workerId: "worker-1",
192
- environment: "local",
193
- baseUrl: "http://localhost:3000",
205
+ const [mockConfig] = await createAndLoadConfig({
206
+ configFile: {
207
+ token: "existing-token",
208
+ workerId: "worker-1",
209
+ environment: "local",
210
+ baseUrl: "http://localhost:3000",
211
+ },
194
212
  });
195
213
 
196
214
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
197
215
 
198
- const setTokenSpy = vi.spyOn(mockConfig, "setToken");
216
+ const updateSpy = vi.spyOn(mockConfig, "update");
199
217
 
200
218
  const context = createBaseContext();
201
219
 
202
220
  await logout.call(context, baseFlags);
203
221
 
204
- expect(setTokenSpy).toHaveBeenCalledWith(null);
222
+ expect(updateSpy).toHaveBeenCalledWith({
223
+ token: null,
224
+ });
205
225
  });
206
226
  });
@@ -28,9 +28,11 @@ export const login = buildHandler(async function (
28
28
  return;
29
29
  }
30
30
 
31
- await this.config.setEnvironment(environment);
32
- await this.config.setToken(token);
33
- await this.config.setWorkerId(null);
31
+ await this.config.update({
32
+ environment,
33
+ token,
34
+ workerId: null,
35
+ });
34
36
 
35
37
  this.writer.writeErr("Successfully logged in!");
36
38
  });
@@ -40,5 +42,7 @@ export const show = buildHandler(function (this: HandlerContext) {
40
42
  });
41
43
 
42
44
  export const logout = buildHandler(async function (this: HandlerContext) {
43
- await this.config.setToken(null);
45
+ await this.config.update({
46
+ token: null,
47
+ });
44
48
  });
@@ -14,8 +14,8 @@ import { downloadBundle } from "./bundle.impl.js";
14
14
  import {
15
15
  baseFlags,
16
16
  cleanupTmpDirectories,
17
+ createAndLoadConfig,
17
18
  createBaseContext,
18
- createTestConfig,
19
19
  } from "./utils/testing.js";
20
20
 
21
21
  afterEach(cleanupTmpDirectories);
@@ -34,12 +34,14 @@ describe("downloadBundle", () => {
34
34
  });
35
35
 
36
36
  it("downloads and pipes bundle to stdout when workerId is set", async () => {
37
- const mockConfig = await createTestConfig({
38
- token:
39
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
40
- workerId: "worker-123",
41
- environment: "local",
42
- baseUrl: "http://localhost:3000",
37
+ const [mockConfig] = await createAndLoadConfig({
38
+ configFile: {
39
+ token:
40
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
41
+ workerId: "worker-123",
42
+ environment: "local",
43
+ baseUrl: "http://localhost:3000",
44
+ },
43
45
  });
44
46
 
45
47
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -76,12 +78,14 @@ describe("downloadBundle", () => {
76
78
  });
77
79
 
78
80
  it("throws error when workerId is not set", async () => {
79
- const mockConfig = await createTestConfig({
80
- token:
81
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
82
- workerId: null,
83
- environment: "local",
84
- baseUrl: "http://localhost:3000",
81
+ const [mockConfig] = await createAndLoadConfig({
82
+ configFile: {
83
+ token:
84
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
85
+ workerId: null,
86
+ environment: "local",
87
+ baseUrl: "http://localhost:3000",
88
+ },
85
89
  });
86
90
 
87
91
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -101,12 +105,14 @@ describe("downloadBundle", () => {
101
105
  });
102
106
 
103
107
  it("handles download failure gracefully", async () => {
104
- const mockConfig = await createTestConfig({
105
- token:
106
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
107
- workerId: "worker-123",
108
- environment: "local",
109
- baseUrl: "http://localhost:3000",
108
+ const [mockConfig] = await createAndLoadConfig({
109
+ configFile: {
110
+ token:
111
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
112
+ workerId: "worker-123",
113
+ environment: "local",
114
+ baseUrl: "http://localhost:3000",
115
+ },
110
116
  });
111
117
 
112
118
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -12,8 +12,8 @@ import { Config } from "../config.js";
12
12
  import {
13
13
  baseFlags,
14
14
  cleanupTmpDirectories,
15
+ createAndLoadConfig,
15
16
  createBaseContext,
16
- createTestConfig,
17
17
  } from "./utils/testing.js";
18
18
 
19
19
  // Mock external dependencies
@@ -41,12 +41,14 @@ describe("deploy", () => {
41
41
  });
42
42
 
43
43
  it("deploys existing worker when workerId is set", async () => {
44
- const mockConfig = await createTestConfig({
45
- token:
46
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
47
- workerId: "worker-123",
48
- environment: "local",
49
- baseUrl: "http://localhost:3000",
44
+ const [mockConfig] = await createAndLoadConfig({
45
+ configFile: {
46
+ token:
47
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
48
+ workerId: "worker-123",
49
+ environment: "local",
50
+ baseUrl: "http://localhost:3000",
51
+ },
50
52
  });
51
53
 
52
54
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -54,7 +56,7 @@ describe("deploy", () => {
54
56
  Result.success({ workerId: "worker-123" }),
55
57
  );
56
58
 
57
- const setWorkerIdSpy = vi.spyOn(mockConfig, "setWorkerId");
59
+ const updateSpy = vi.spyOn(mockConfig, "update");
58
60
  const context = createBaseContext();
59
61
 
60
62
  await deploy.call(context, baseFlags);
@@ -67,7 +69,9 @@ describe("deploy", () => {
67
69
  environment: "local",
68
70
  });
69
71
 
70
- expect(setWorkerIdSpy).toHaveBeenCalledWith("worker-123");
72
+ expect(updateSpy).toHaveBeenCalledWith({
73
+ workerId: "worker-123",
74
+ });
71
75
 
72
76
  const allCalls = stderrSpy.mock.calls.map((call) => call[0]).join("");
73
77
  expect(allCalls).toContain("Deploying worker...");
@@ -75,12 +79,14 @@ describe("deploy", () => {
75
79
  });
76
80
 
77
81
  it("deploys new worker with name flag", async () => {
78
- const mockConfig = await createTestConfig({
79
- token:
80
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
81
- workerId: null,
82
- environment: "local",
83
- baseUrl: "http://localhost:3000",
82
+ const [mockConfig] = await createAndLoadConfig({
83
+ configFile: {
84
+ token:
85
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
86
+ workerId: null,
87
+ environment: "local",
88
+ baseUrl: "http://localhost:3000",
89
+ },
84
90
  });
85
91
 
86
92
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -88,7 +94,7 @@ describe("deploy", () => {
88
94
  Result.success({ workerId: "worker-456" }),
89
95
  );
90
96
 
91
- const setWorkerIdSpy = vi.spyOn(mockConfig, "setWorkerId");
97
+ const updateSpy = vi.spyOn(mockConfig, "update");
92
98
  const context = createBaseContext();
93
99
 
94
100
  await deploy.call(context, { ...baseFlags, name: "my-worker" });
@@ -101,7 +107,9 @@ describe("deploy", () => {
101
107
  environment: "local",
102
108
  });
103
109
 
104
- expect(setWorkerIdSpy).toHaveBeenCalledWith("worker-456");
110
+ expect(updateSpy).toHaveBeenCalledWith({
111
+ workerId: "worker-456",
112
+ });
105
113
  expect(prompts).not.toHaveBeenCalled();
106
114
 
107
115
  const allCalls = stderrSpy.mock.calls.map((call) => call[0]).join("");
@@ -109,12 +117,14 @@ describe("deploy", () => {
109
117
  });
110
118
 
111
119
  it("prompts for name when deploying new worker without name flag", async () => {
112
- const mockConfig = await createTestConfig({
113
- token:
114
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
115
- workerId: null,
116
- environment: "local",
117
- baseUrl: "http://localhost:3000",
120
+ const [mockConfig] = await createAndLoadConfig({
121
+ configFile: {
122
+ token:
123
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
124
+ workerId: null,
125
+ environment: "local",
126
+ baseUrl: "http://localhost:3000",
127
+ },
118
128
  });
119
129
 
120
130
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -123,7 +133,7 @@ describe("deploy", () => {
123
133
  Result.success({ workerId: "worker-789" }),
124
134
  );
125
135
 
126
- const setWorkerIdSpy = vi.spyOn(mockConfig, "setWorkerId");
136
+ const updateSpy = vi.spyOn(mockConfig, "update");
127
137
  const context = createBaseContext();
128
138
 
129
139
  await deploy.call(context, baseFlags);
@@ -143,16 +153,20 @@ describe("deploy", () => {
143
153
  environment: "local",
144
154
  });
145
155
 
146
- expect(setWorkerIdSpy).toHaveBeenCalledWith("worker-789");
156
+ expect(updateSpy).toHaveBeenCalledWith({
157
+ workerId: "worker-789",
158
+ });
147
159
  });
148
160
 
149
161
  it("throws error when prompt is cancelled", async () => {
150
- const mockConfig = await createTestConfig({
151
- token:
152
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
153
- workerId: null,
154
- environment: "local",
155
- baseUrl: "http://localhost:3000",
162
+ const [mockConfig] = await createAndLoadConfig({
163
+ configFile: {
164
+ token:
165
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
166
+ workerId: null,
167
+ environment: "local",
168
+ baseUrl: "http://localhost:3000",
169
+ },
156
170
  });
157
171
 
158
172
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -166,12 +180,14 @@ describe("deploy", () => {
166
180
  });
167
181
 
168
182
  it("throws error when both workerId and name are provided", async () => {
169
- const mockConfig = await createTestConfig({
170
- token:
171
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
172
- workerId: "worker-123",
173
- environment: "local",
174
- baseUrl: "http://localhost:3000",
183
+ const [mockConfig] = await createAndLoadConfig({
184
+ configFile: {
185
+ token:
186
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
187
+ workerId: "worker-123",
188
+ environment: "local",
189
+ baseUrl: "http://localhost:3000",
190
+ },
175
191
  });
176
192
 
177
193
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -184,12 +200,14 @@ describe("deploy", () => {
184
200
  });
185
201
 
186
202
  it("handles deployment failure", async () => {
187
- const mockConfig = await createTestConfig({
188
- token:
189
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
190
- workerId: "worker-123",
191
- environment: "local",
192
- baseUrl: "http://localhost:3000",
203
+ const [mockConfig] = await createAndLoadConfig({
204
+ configFile: {
205
+ token:
206
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
207
+ workerId: "worker-123",
208
+ environment: "local",
209
+ baseUrl: "http://localhost:3000",
210
+ },
193
211
  });
194
212
 
195
213
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -211,12 +229,14 @@ describe("deploy", () => {
211
229
  });
212
230
 
213
231
  it("trims whitespace from prompted name", async () => {
214
- const mockConfig = await createTestConfig({
215
- token:
216
- "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
217
- workerId: null,
218
- environment: "local",
219
- baseUrl: "http://localhost:3000",
232
+ const [mockConfig] = await createAndLoadConfig({
233
+ configFile: {
234
+ token:
235
+ "1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig",
236
+ workerId: null,
237
+ environment: "local",
238
+ baseUrl: "http://localhost:3000",
239
+ },
220
240
  });
221
241
 
222
242
  vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
@@ -68,7 +68,7 @@ export const deploy = buildAuthedHandler(async function (flags: DeployFlags) {
68
68
 
69
69
  if (Result.isSuccess(result)) {
70
70
  const { workerId } = Result.unwrap(result);
71
- await this.config.setWorkerId(workerId);
71
+ await this.config.update({ workerId });
72
72
  this.writer.writeErr("✓ Successfully deployed worker");
73
73
  } else {
74
74
  this.writer.writeErr("✗ Failed to deploy worker");
@@ -3,41 +3,37 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import * as os from "node:os";
4
4
  import { tmpdir } from "node:os";
5
5
  import * as path from "node:path";
6
- import { Config, type Environment } from "../../config.js";
6
+ import { Config, type ConfigMap } from "../../config.js";
7
7
  import type { GlobalFlags } from "../../flags.js";
8
8
  import { Writer } from "../../writer.js";
9
9
 
10
- export type ConfigFileContents = {
11
- token: string | null;
12
- workerId: string | null;
13
- environment: Environment;
14
- baseUrl: string;
15
- };
16
-
17
10
  export const tmpDirectories: string[] = [];
18
11
 
19
12
  export const baseFlags: GlobalFlags = {
20
13
  debug: false,
21
14
  };
22
15
 
23
- export async function createConfigFile(contents: ConfigFileContents) {
24
- const dir = await mkdtemp(path.join(tmpdir(), "cmd-test-"));
25
- tmpDirectories.push(dir);
26
- const configFilePath = path.join(dir, "config.json");
27
-
28
- await writeFile(configFilePath, JSON.stringify(contents, null, 2), "utf-8");
29
- return configFilePath;
30
- }
16
+ export async function createAndLoadConfig({
17
+ configFile,
18
+ env,
19
+ flags,
20
+ }: {
21
+ configFile: Partial<ConfigMap>;
22
+ env?: Partial<NodeJS.ProcessEnv>;
23
+ flags?: Partial<GlobalFlags>;
24
+ }): Promise<[config: Config, path: string]> {
25
+ const configFilePath = await createConfigFile(configFile);
31
26
 
32
- export async function createTestConfig(
33
- configFileContents: ConfigFileContents,
34
- ): Promise<Config> {
35
- const configFilePath = await createConfigFile(configFileContents);
36
- return Config.load({
27
+ const map = await Config.load({
37
28
  configFilePath,
38
- processEnv: {} as NodeJS.ProcessEnv,
39
- flags: { debug: false },
29
+ processEnv: env ?? {},
30
+ flags: {
31
+ ...baseFlags,
32
+ ...(flags ?? {}),
33
+ },
40
34
  });
35
+
36
+ return [map, configFilePath];
41
37
  }
42
38
 
43
39
  export function createBaseContext() {
@@ -58,3 +54,12 @@ export async function cleanupTmpDirectories() {
58
54
  }
59
55
  }
60
56
  }
57
+
58
+ async function createConfigFile(contents: Partial<ConfigMap>) {
59
+ const dir = await mkdtemp(path.join(tmpdir(), "cmd-test-"));
60
+ tmpDirectories.push(dir);
61
+ const configFilePath = path.join(dir, "config.json");
62
+
63
+ await writeFile(configFilePath, JSON.stringify(contents, null, 2), "utf-8");
64
+ return configFilePath;
65
+ }
@@ -1,74 +1,30 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import * as path from "node:path";
1
+ import { readFile } from "node:fs/promises";
4
2
  import { afterEach, describe, expect, it } from "vitest";
5
- import { Config, type Environment } from "./config.js";
6
- import type { GlobalFlags } from "./flags.js";
7
-
8
- type ConfigFileContents = {
9
- token: string | null;
10
- workerId: string | null;
11
- environment: Environment;
12
- baseUrl: string;
13
- };
14
-
15
- const tmpDirectories: string[] = [];
16
-
17
- async function createConfigFile(contents: ConfigFileContents) {
18
- const dir = await mkdtemp(path.join(tmpdir(), "config-test-"));
19
- tmpDirectories.push(dir);
20
- const configFilePath = path.join(dir, "config.json");
3
+ import {
4
+ cleanupTmpDirectories,
5
+ createAndLoadConfig,
6
+ } from "./commands/utils/testing.js";
7
+ import { Config, type ConfigMap } from "./config.js";
21
8
 
22
- await writeFile(configFilePath, JSON.stringify(contents, null, 2), "utf-8");
23
- return configFilePath;
24
- }
25
-
26
- afterEach(async () => {
27
- while (tmpDirectories.length > 0) {
28
- const dir = tmpDirectories.pop();
29
- if (dir) {
30
- await rm(dir, { recursive: true, force: true });
31
- }
32
- }
33
- });
34
-
35
- async function loadTestConfig({
36
- configFile,
37
- env,
38
- flags,
39
- }: {
40
- configFile: ConfigFileContents;
41
- env?: Partial<NodeJS.ProcessEnv>;
42
- flags?: Partial<GlobalFlags>;
43
- }) {
44
- const configFilePath = await createConfigFile(configFile);
45
- return Config.load({
46
- configFilePath,
47
- processEnv: { ...(env ?? {}) } as NodeJS.ProcessEnv,
48
- flags: {
49
- debug: false,
50
- ...(flags ?? {}),
51
- },
52
- });
53
- }
54
-
55
- const baseConfigFile: ConfigFileContents = {
9
+ const baseConfigMap: ConfigMap = {
56
10
  token: "file-token",
57
11
  workerId: "file-worker",
58
12
  environment: "local",
59
13
  baseUrl: "https://config.example.com",
60
14
  };
61
15
 
16
+ afterEach(cleanupTmpDirectories);
17
+
62
18
  describe("Config.load precedence", () => {
63
19
  it("uses values from the config file when no overrides are present", async () => {
64
- const config = await loadTestConfig({
65
- configFile: baseConfigFile,
20
+ const [config] = await createAndLoadConfig({
21
+ configFile: baseConfigMap,
66
22
  });
67
23
 
68
- expect(config.token).toBe(baseConfigFile.token);
69
- expect(config.workerId).toBe(baseConfigFile.workerId);
70
- expect(config.environment).toBe(baseConfigFile.environment);
71
- expect(config.baseUrl).toBe(baseConfigFile.baseUrl);
24
+ expect(config.token).toBe(baseConfigMap.token);
25
+ expect(config.workerId).toBe(baseConfigMap.workerId);
26
+ expect(config.environment).toBe(baseConfigMap.environment);
27
+ expect(config.baseUrl).toBe(baseConfigMap.baseUrl);
72
28
  });
73
29
 
74
30
  it("prefers environment variables over the config file", async () => {
@@ -77,15 +33,15 @@ describe("Config.load precedence", () => {
77
33
  WORKERS_WORKER_ID: "env-worker",
78
34
  WORKERS_ENVIRONMENT: "staging",
79
35
  };
80
- const config = await loadTestConfig({
81
- configFile: baseConfigFile,
36
+ const [config] = await createAndLoadConfig({
37
+ configFile: baseConfigMap,
82
38
  env: envVars,
83
39
  });
84
40
 
85
41
  expect(config.token).toBe(envVars.WORKERS_TOKEN);
86
42
  expect(config.workerId).toBe(envVars.WORKERS_WORKER_ID);
87
43
  expect(config.environment).toBe(envVars.WORKERS_ENVIRONMENT);
88
- expect(config.baseUrl).toBe(baseConfigFile.baseUrl);
44
+ expect(config.baseUrl).toBe(baseConfigMap.baseUrl);
89
45
  });
90
46
 
91
47
  it("prefers flags over environment variables and config file values", async () => {
@@ -100,59 +56,53 @@ describe("Config.load precedence", () => {
100
56
  "base-url": "https://flag.example.com",
101
57
  };
102
58
 
103
- const config = await loadTestConfig({
104
- configFile: baseConfigFile,
59
+ const [config] = await createAndLoadConfig({
60
+ configFile: baseConfigMap,
105
61
  env: envVars,
106
62
  flags,
107
63
  });
108
64
 
109
- expect(config.token).toBe(baseConfigFile.token);
65
+ expect(config.token).toBe(baseConfigMap.token);
110
66
  expect(config.workerId).toBe(envVars.WORKERS_WORKER_ID);
111
67
  expect(config.environment).toBe(flags.env);
112
68
  expect(config.baseUrl).toBe(flags["base-url"]);
113
69
  });
114
70
  });
115
71
 
116
- describe("Config.setEnvironment", () => {
117
- it("updates the baseUrl when environment is changed", async () => {
118
- const config = await loadTestConfig({
72
+ describe("Config.update", () => {
73
+ it("persists only provided keys to disk", async () => {
74
+ const [, configFilePath] = await createAndLoadConfig({
119
75
  configFile: {
120
- token: "test-token",
121
- workerId: null,
76
+ token: "file-token",
77
+ workerId: "file-worker",
122
78
  environment: "prod",
123
- baseUrl: "https://www.notion.so",
79
+ baseUrl: "https://config.example.com",
124
80
  },
125
81
  });
126
82
 
127
- expect(config.environment).toBe("prod");
128
- expect(config.baseUrl).toBe("https://www.notion.so");
129
-
130
- await config.setEnvironment("local");
83
+ const config = await Config.load({
84
+ configFilePath,
85
+ processEnv: {
86
+ WORKERS_BASE_URL: "https://env.example.com",
87
+ } as NodeJS.ProcessEnv,
88
+ flags: { debug: false },
89
+ });
131
90
 
132
- expect(config.environment).toBe("local");
133
- expect(config.baseUrl).toBe("http://localhost:3000");
134
- });
91
+ expect(config.baseUrl).toBe("https://env.example.com");
135
92
 
136
- it("updates baseUrl for all environments", async () => {
137
- const config = await loadTestConfig({
138
- configFile: {
139
- token: "test-token",
140
- workerId: null,
141
- environment: "prod",
142
- baseUrl: "https://www.notion.so",
143
- },
93
+ await config.update({
94
+ token: "updated-token",
144
95
  });
145
96
 
146
- await config.setEnvironment("staging");
147
- expect(config.baseUrl).toBe("https://staging.notion.so");
97
+ expect(config.token).toBe("updated-token");
148
98
 
149
- await config.setEnvironment("dev");
150
- expect(config.baseUrl).toBe("https://dev.notion.so");
99
+ const persisted = JSON.parse(await readFile(configFilePath, "utf-8"));
151
100
 
152
- await config.setEnvironment("prod");
153
- expect(config.baseUrl).toBe("https://www.notion.so");
154
-
155
- await config.setEnvironment("local");
156
- expect(config.baseUrl).toBe("http://localhost:3000");
101
+ expect(persisted).toEqual({
102
+ token: "updated-token",
103
+ workerId: "file-worker",
104
+ environment: "prod",
105
+ baseUrl: "https://config.example.com",
106
+ });
157
107
  });
158
108
  });
package/src/cli/config.ts CHANGED
@@ -5,7 +5,7 @@ import type { GlobalFlags } from "./flags.js";
5
5
  export const Environments = ["local", "staging", "dev", "prod"] as const;
6
6
  export type Environment = (typeof Environments)[number];
7
7
 
8
- interface ConfigMap {
8
+ export interface ConfigMap {
9
9
  environment: Environment;
10
10
  baseUrl: string;
11
11
  token: string | null;
@@ -46,11 +46,11 @@ export class Config {
46
46
  readonly #configFilePath: string;
47
47
 
48
48
  constructor(opts: {
49
+ configMap: ConfigMap;
49
50
  configFilePath: string;
50
- configFile: ConfigMap;
51
51
  }) {
52
+ this.#configMap = opts.configMap;
52
53
  this.#configFilePath = opts.configFilePath;
53
- this.#configMap = opts.configFile;
54
54
  }
55
55
 
56
56
  // Getters read from the environment variables, and then the config file.
@@ -71,32 +71,6 @@ export class Config {
71
71
  return this.#configMap.workerId;
72
72
  }
73
73
 
74
- // Setters update the config file, and then write it to disk.
75
-
76
- async setEnvironment(environment: Environment) {
77
- this.#configMap.environment = environment;
78
- this.#configMap.baseUrl = baseUrl(environment);
79
- await this.#writeConfigFile();
80
- }
81
-
82
- async setToken(token: string | null) {
83
- this.#configMap.token = token;
84
- await this.#writeConfigFile();
85
- }
86
-
87
- async setWorkerId(workerId: string | null) {
88
- this.#configMap.workerId = workerId;
89
- await this.#writeConfigFile();
90
- }
91
-
92
- async #writeConfigFile() {
93
- await fs.promises.writeFile(
94
- this.#configFilePath,
95
- JSON.stringify(this.#configMap, null, 2),
96
- "utf-8",
97
- );
98
- }
99
-
100
74
  get tokenInfo(): { token: string; spaceId: string; cellId: string } {
101
75
  const token = this.token;
102
76
  if (!token) {
@@ -107,47 +81,100 @@ export class Config {
107
81
  return { token, spaceId, cellId };
108
82
  }
109
83
 
84
+ /**
85
+ * Update the config with a partial config map.
86
+ *
87
+ * This will write only the updated keys in the config file on disk. Not all
88
+ * keys are written, since some current keys in the Config object may have
89
+ * come from e.g. environment variables, rather than the original config
90
+ * file.
91
+ *
92
+ * @param config The config update.
93
+ */
94
+ async update(config: Partial<ConfigMap>) {
95
+ Object.assign(this.#configMap, config);
96
+
97
+ const currentConfigFile = await Config.#readConfigFile(
98
+ this.#configFilePath,
99
+ );
100
+ Object.assign(currentConfigFile, config);
101
+ await fs.promises.writeFile(
102
+ this.#configFilePath,
103
+ JSON.stringify(currentConfigFile, null, 2),
104
+ "utf-8",
105
+ );
106
+ }
107
+
110
108
  static async load(opts: {
111
109
  configFilePath: string;
112
110
  processEnv: NodeJS.ProcessEnv;
113
111
  flags: GlobalFlags;
114
112
  }) {
115
113
  const absConfigFilePath = path.resolve(process.cwd(), opts.configFilePath);
116
- const configFile = await Config.#readConfigFile(absConfigFilePath);
114
+ const partialConfig = await Config.#readConfigFile(absConfigFilePath);
117
115
 
118
116
  if (opts.processEnv.WORKERS_TOKEN) {
119
- configFile.token = opts.processEnv.WORKERS_TOKEN;
117
+ partialConfig.token = opts.processEnv.WORKERS_TOKEN;
120
118
  }
121
119
  if (opts.processEnv.WORKERS_ENVIRONMENT) {
122
- configFile.environment = parseEnvironment(
120
+ partialConfig.environment = parseEnvironment(
123
121
  opts.processEnv.WORKERS_ENVIRONMENT,
124
122
  );
125
123
  }
126
124
  if (opts.processEnv.WORKERS_WORKER_ID) {
127
- configFile.workerId = opts.processEnv.WORKERS_WORKER_ID;
125
+ partialConfig.workerId = opts.processEnv.WORKERS_WORKER_ID;
128
126
  }
129
127
  if (opts.processEnv.WORKERS_BASE_URL) {
130
- configFile.baseUrl = opts.processEnv.WORKERS_BASE_URL;
128
+ partialConfig.baseUrl = opts.processEnv.WORKERS_BASE_URL;
131
129
  }
132
130
 
133
131
  if (opts.flags.token) {
134
- configFile.token = opts.flags.token;
132
+ partialConfig.token = opts.flags.token;
135
133
  }
136
134
  if (opts.flags.env) {
137
- configFile.environment = parseEnvironment(opts.flags.env);
135
+ partialConfig.environment = parseEnvironment(opts.flags.env);
138
136
  }
139
137
  if (opts.flags["base-url"]) {
140
- configFile.baseUrl = opts.flags["base-url"];
138
+ partialConfig.baseUrl = opts.flags["base-url"];
141
139
  }
142
140
  if (opts.flags["worker-id"]) {
143
- configFile.workerId = opts.flags["worker-id"];
141
+ partialConfig.workerId = opts.flags["worker-id"];
142
+ }
143
+
144
+ partialConfig.baseUrl ??= baseUrlForEnvironment(
145
+ partialConfig.environment ?? "prod",
146
+ );
147
+
148
+ const environment = partialConfig.environment;
149
+ if (!environment) {
150
+ throw new Error("Environment is required");
151
+ }
152
+
153
+ const baseUrl = partialConfig.baseUrl;
154
+ if (!baseUrl) {
155
+ throw new Error("Base URL is required");
156
+ }
157
+
158
+ const token = partialConfig.token;
159
+ if (token === undefined) {
160
+ throw new Error("Token is required");
161
+ }
162
+
163
+ const workerId = partialConfig.workerId;
164
+ if (workerId === undefined) {
165
+ throw new Error("Worker ID is required");
144
166
  }
145
167
 
146
- configFile.baseUrl ??= baseUrl(configFile.environment ?? "prod");
168
+ const configMap: ConfigMap = {
169
+ environment,
170
+ baseUrl,
171
+ token,
172
+ workerId,
173
+ };
147
174
 
148
175
  return new Config({
149
176
  configFilePath: absConfigFilePath,
150
- configFile: configFile as ConfigMap,
177
+ configMap,
151
178
  });
152
179
  }
153
180
 
@@ -168,7 +195,7 @@ export class Config {
168
195
  token: null,
169
196
  workerId: null,
170
197
  environment: "prod",
171
- baseUrl: baseUrl("prod"),
198
+ baseUrl: baseUrlForEnvironment("prod"),
172
199
  };
173
200
  } else {
174
201
  throw error;
@@ -227,7 +254,7 @@ export function extractPayloadFromToken(token: string): {
227
254
  }
228
255
  }
229
256
 
230
- function baseUrl(environment: Environment): string {
257
+ function baseUrlForEnvironment(environment: Environment): string {
231
258
  switch (environment) {
232
259
  case "local":
233
260
  return "http://localhost:3000";