@catladder/cli 0.0.0-refactor-cli-add-improved-git-utilities-224e6ee7 → 0.0.0-refactor-cli-add-improved-git-utilities-916545cc

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 (41) hide show
  1. package/dist/bundles/catenv/index.js +43680 -26855
  2. package/dist/bundles/cli/index.js +95077 -78240
  3. package/dist/cli/src/apps/cli/commands/project/setup/setupAccessTokens.d.ts +2 -1
  4. package/dist/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js +3 -1
  5. package/dist/cli/src/apps/cli/commands/project/setup/setupAccessTokens.js.map +1 -1
  6. package/dist/cli/src/apps/cli/commands/project/setup/setupCloudRun.js +4 -2
  7. package/dist/cli/src/apps/cli/commands/project/setup/setupCloudRun.js.map +1 -1
  8. package/dist/cli/src/apps/cli/commands/project/setup/setupKubernetes.d.ts +2 -1
  9. package/dist/cli/src/apps/cli/commands/project/setup/setupKubernetes.js +23 -9
  10. package/dist/cli/src/apps/cli/commands/project/setup/setupKubernetes.js.map +1 -1
  11. package/dist/cli/src/apps/cli/commands/project/setup/setupTopic.js +6 -4
  12. package/dist/cli/src/apps/cli/commands/project/setup/setupTopic.js.map +1 -1
  13. package/dist/cli/src/gitlab/apiClient.d.ts +3 -1
  14. package/dist/cli/src/gitlab/apiClient.js +2 -1
  15. package/dist/cli/src/gitlab/apiClient.js.map +1 -1
  16. package/dist/cli/src/gitlab/getGitlabProjectId.d.ts +4 -0
  17. package/dist/cli/src/gitlab/getGitlabProjectId.js +29 -0
  18. package/dist/cli/src/gitlab/getGitlabProjectId.js.map +1 -0
  19. package/dist/cli/src/gitlab/index.d.ts +3 -0
  20. package/dist/cli/src/gitlab/index.js +20 -0
  21. package/dist/cli/src/gitlab/index.js.map +1 -0
  22. package/dist/cli/src/utils/gitlab.d.ts +9 -6
  23. package/dist/cli/src/utils/gitlab.js +59 -42
  24. package/dist/cli/src/utils/gitlab.js.map +1 -1
  25. package/dist/cli/src/utils/preferences.js +23 -13
  26. package/dist/cli/src/utils/preferences.js.map +1 -1
  27. package/dist/cli/src/utils/projectPreferences.d.ts +5 -0
  28. package/dist/cli/src/utils/projectPreferences.js +54 -0
  29. package/dist/cli/src/utils/projectPreferences.js.map +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +1 -1
  32. package/src/apps/cli/commands/project/setup/setupAccessTokens.ts +11 -1
  33. package/src/apps/cli/commands/project/setup/setupCloudRun.ts +4 -2
  34. package/src/apps/cli/commands/project/setup/setupKubernetes.ts +30 -27
  35. package/src/apps/cli/commands/project/setup/setupTopic.ts +6 -11
  36. package/src/gitlab/apiClient.ts +5 -2
  37. package/src/gitlab/getGitlabProjectId.ts +32 -0
  38. package/src/gitlab/index.ts +3 -0
  39. package/src/utils/gitlab.ts +88 -96
  40. package/src/utils/preferences.ts +40 -20
  41. package/src/utils/projectPreferences.ts +78 -0
package/package.json CHANGED
@@ -53,7 +53,7 @@
53
53
  }
54
54
  ],
55
55
  "license": "MIT",
56
- "version": "0.0.0-refactor-cli-add-improved-git-utilities-224e6ee7",
56
+ "version": "0.0.0-refactor-cli-add-improved-git-utilities-916545cc",
57
57
  "scripts": {
58
58
  "lint": "eslint \"src/**/*.ts\"",
59
59
  "lint:fix": "eslint \"src/**/*.ts\" --fix",
@@ -1,10 +1,20 @@
1
1
  import type { CommandInstance } from "vorpal";
2
2
  import dayjs from "dayjs";
3
3
  import { doGitlabRequest, getProjectInfo } from "../../../../../utils/gitlab";
4
+ import {
5
+ getGitlabProjectId,
6
+ gitlabApiClient,
7
+ type GitlabRestClient,
8
+ } from "../../../../../gitlab";
4
9
 
5
10
  const TOKEN_NAME = "semantic-release";
6
11
 
7
- export const setupAccessTokens = async (instance: CommandInstance) => {
12
+ export const setupAccessTokens = async (
13
+ instance: CommandInstance,
14
+ preinit?: [number, GitlabRestClient],
15
+ ) => {
16
+ const [id, gitlabApi] =
17
+ preinit ?? (await Promise.all([getGitlabProjectId(), gitlabApiClient()]));
8
18
  const { id: projectId } = await getProjectInfo(instance);
9
19
 
10
20
  const projectTokens = await doGitlabRequest(
@@ -78,7 +78,9 @@ export const setupCloudRun = async (
78
78
  },
79
79
  context.env,
80
80
  context.name,
81
- false, // backup
82
- false, // masked
81
+ {
82
+ backup: false,
83
+ masked: false,
84
+ },
83
85
  );
84
86
  };
@@ -6,27 +6,30 @@ import {
6
6
  import type { CommandInstance } from "vorpal";
7
7
  import { exec } from "child-process-promise";
8
8
  import { connectToCluster } from "../../../../../utils/cluster";
9
- import {
10
- doGitlabRequest,
11
- getProjectInfo,
12
- upsertAllVariables,
13
- } from "../../../../../utils/gitlab";
9
+ import { upsertAllVariables } from "../../../../../utils/gitlab";
14
10
  import ensureNamespace from "../utils/ensureNamespace";
11
+ import type { GitlabRestClient } from "../../../../../gitlab";
12
+ import { getGitlabProjectId, gitlabApiClient } from "../../../../../gitlab";
15
13
 
16
14
  export const setupKubernetes = async (
17
15
  instance: CommandInstance,
18
16
  context: ComponentContext,
17
+ preinit?: [number, GitlabRestClient],
19
18
  ) => {
20
19
  const deployConfig = context.deploy?.config;
21
20
  if (!isOfDeployType(deployConfig, "kubernetes")) {
22
21
  throw new Error("cannot run setupKubernetes on non-kubernetes deployments");
23
22
  }
23
+ const [id, gitlabApi] =
24
+ preinit ?? (await Promise.all([getGitlabProjectId(), gitlabApiClient()]));
24
25
 
25
- const { id: projectId } = await getProjectInfo(instance);
26
- const deploy_tokens = await doGitlabRequest(
27
- instance,
28
- `projects/${projectId}/deploy_tokens`,
29
- );
26
+ const deploy_tokens = await gitlabApi.DeployTokens.all({ projectId: id });
27
+
28
+ // const { id: projectId } = await getProjectInfo(instance);
29
+ // const deploy_tokens = await doGitlabRequest(
30
+ // instance,
31
+ // `projects/${projectId}/deploy_tokens`,
32
+ // );
30
33
 
31
34
  if (
32
35
  !deploy_tokens.find(
@@ -36,17 +39,21 @@ export const setupKubernetes = async (
36
39
  instance.log(
37
40
  "I will setup the 'GitLab Deploy Token', so Kubernetes can pull images from this project.",
38
41
  );
39
-
40
- await doGitlabRequest(
41
- instance,
42
- `projects/${projectId}/deploy_tokens`,
43
- {
44
- id: projectId,
45
- name: "gitlab-deploy-token",
46
- scopes: ["read_registry"],
47
- },
48
- "POST",
42
+ await gitlabApi.DeployTokens.create(
43
+ "gitlab-deploy-token",
44
+ ["read_registry"],
45
+ { projectId: id },
49
46
  );
47
+ // await doGitlabRequest(
48
+ // instance,
49
+ // `projects/${projectId}/deploy_tokens`,
50
+ // {
51
+ // id: projectId,
52
+ // name: "gitlab-deploy-token",
53
+ // scopes: ["read_registry"],
54
+ // },
55
+ // "POST",
56
+ // );
50
57
  }
51
58
 
52
59
  const fullName = getFullKubernetesClusterName(deployConfig.cluster);
@@ -160,12 +167,8 @@ EOF
160
167
  instance.log("");
161
168
  instance.log("pusing secrets to gitlab...");
162
169
 
163
- await upsertAllVariables(
164
- instance,
165
- vars,
166
- context.env,
167
- context.name,
168
- false, // no backup
169
- );
170
+ await upsertAllVariables(instance, vars, context.env, context.name, {
171
+ backup: false,
172
+ });
170
173
  instance.log("done!");
171
174
  };
@@ -1,17 +1,12 @@
1
1
  import type { CommandInstance } from "vorpal";
2
- import { doGitlabRequest, getProjectInfo } from "../../../../../utils/gitlab";
2
+ import { getGitlabProjectInfo, gitlabApiClient } from "../../../../../gitlab";
3
3
 
4
4
  const catladderTopic = "catladder";
5
5
 
6
6
  export const setupTopic = async (instance: CommandInstance) => {
7
- const { id: projectId } = await getProjectInfo(instance);
8
-
9
- const { topics } = await doGitlabRequest(instance, `projects/${projectId}`);
10
-
11
- await doGitlabRequest(
12
- instance,
13
- `projects/${projectId}`,
14
- { topics: [catladderTopic, ...topics] },
15
- "PUT",
16
- );
7
+ const api = await gitlabApiClient();
8
+ const info = await getGitlabProjectInfo(api);
9
+ await api.Projects.edit(info.id, {
10
+ topics: [catladderTopic, ...info.topics],
11
+ });
17
12
  };
@@ -1,11 +1,13 @@
1
1
  import { Gitlab } from "@gitbeaker/rest";
2
2
  import { getGitlabToken } from "./gitlabToken";
3
3
 
4
+ export type GitlabRestClient = InstanceType<typeof Gitlab<false>>;
5
+
4
6
  export const gitlabApiClient = async ({
5
7
  gitlabHost,
6
8
  }: {
7
9
  gitlabHost?: string;
8
- } = {}) => {
10
+ } = {}): Promise<GitlabRestClient> => {
9
11
  const { token, gitRemoteHost } = await getGitlabToken(gitlabHost);
10
12
  if (typeof token !== "string") {
11
13
  throw new Error("Gitlab personal access token is missing!");
@@ -15,6 +17,7 @@ export const gitlabApiClient = async ({
15
17
  }
16
18
  return new Gitlab({
17
19
  token,
18
- host: gitRemoteHost,
20
+ host: `https://${gitRemoteHost}`,
21
+ camelize: false,
19
22
  });
20
23
  };
@@ -0,0 +1,32 @@
1
+ import type { ProjectSchema } from "@gitbeaker/rest";
2
+ import { getGitRemoteHostAndPath } from "../git/getGitRemoteHostAndPath";
3
+ import type { GitlabRestClient } from "./apiClient";
4
+ import { gitlabApiClient } from "./apiClient";
5
+ import {
6
+ upsertProjectPerferences,
7
+ getProjectPreferences,
8
+ } from "../utils/projectPreferences";
9
+
10
+ export const getGitlabProjectInfo = async (
11
+ existingInstance?: GitlabRestClient,
12
+ ): Promise<ProjectSchema> => {
13
+ const api = existingInstance ?? (await gitlabApiClient());
14
+ const { gitRemotePath } = await getGitRemoteHostAndPath();
15
+ const projectInfo = await api.Projects.show(gitRemotePath, {
16
+ statistics: false,
17
+ withCustomAttributes: false,
18
+ license: false,
19
+ });
20
+ // write unawaited intentionally
21
+ upsertProjectPerferences({ gitlabProjectId: projectInfo.id });
22
+ return projectInfo;
23
+ };
24
+
25
+ export const getGitlabProjectId = async (): Promise<number> => {
26
+ const { gitlabProjectId } = await getProjectPreferences();
27
+ if (gitlabProjectId) {
28
+ return gitlabProjectId;
29
+ }
30
+ const projectInfo = await getGitlabProjectInfo();
31
+ return projectInfo.id;
32
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./apiClient";
2
+ export * from "./getGitlabProjectId";
3
+ export * from "./gitlabToken";
@@ -5,7 +5,16 @@ import memoizee from "memoizee";
5
5
  import fetch from "node-fetch";
6
6
  import open from "open";
7
7
  import type { CommandInstance } from "vorpal";
8
- import { getPreference, hasPreference, setPreference } from "./preferences";
8
+ import { hasPreference } from "./preferences";
9
+ import {
10
+ setGitlabToken,
11
+ getGitlabToken as getGitlabTokenFromPerferences,
12
+ getGitlabProjectId,
13
+ getGitlabProjectInfo,
14
+ gitlabApiClient,
15
+ type GitlabRestClient,
16
+ } from "../gitlab";
17
+ import type { Camelize, ProjectVariableSchema } from "@gitbeaker/rest";
9
18
 
10
19
  export const hasGitlabToken = async () =>
11
20
  await hasPreference("gitlab-personal-access-token");
@@ -23,6 +32,10 @@ export const setupGitlabToken = async (vorpal: CommandInstance) => {
23
32
  name: "shouldContinue",
24
33
  type: "prompt",
25
34
  });
35
+ if (!shouldContinue) {
36
+ console.info("Aborted by user");
37
+ process.exit(0);
38
+ }
26
39
 
27
40
  open("https://git.panter.ch/-/profile/personal_access_tokens");
28
41
 
@@ -35,13 +48,14 @@ export const setupGitlabToken = async (vorpal: CommandInstance) => {
35
48
  message: "Your personal access token ",
36
49
  });
37
50
  if (typeof personalToken === "string" && personalToken.length) {
38
- await setPreference("gitlab-personal-access-token", personalToken);
51
+ await setGitlabToken(personalToken);
39
52
  } else {
40
53
  console.error("⚠️ no gitlab personal access token set");
41
54
  }
42
55
  };
43
56
  export const getGitlabToken = async (vorpal: CommandInstance | null) => {
44
- if (!(await hasGitlabToken())) {
57
+ const { token } = await getGitlabTokenFromPerferences();
58
+ if (!token?.length) {
45
59
  if (!vorpal) {
46
60
  console.error(
47
61
  "⚠️ gitlab token missing, please run catladder to set it up",
@@ -50,7 +64,7 @@ export const getGitlabToken = async (vorpal: CommandInstance | null) => {
50
64
  }
51
65
  await setupGitlabToken(vorpal);
52
66
  }
53
- return getPreference("gitlab-personal-access-token");
67
+ return token;
54
68
  };
55
69
 
56
70
  type Method = "GET" | "PUT" | "POST" | "DELETE";
@@ -93,22 +107,8 @@ export const doGitlabRequest = async <T = any>(
93
107
  );
94
108
  };
95
109
 
96
- export const getProjectInfo = async (
97
- vorpal: CommandInstance | null,
98
- ): Promise<{ id: string; web_url: string }> => {
99
- const gitRemoteOriginUrl = (
100
- await exec("git config --get remote.origin.url")
101
- ).stdout.trim();
102
- const projectPath =
103
- /(https:\/\/|git@)git\.panter\.ch[:/]([^.]*)(\.git)?/g.exec(
104
- gitRemoteOriginUrl,
105
- );
106
- const project = await doGitlabRequest(
107
- vorpal,
108
- `projects/${encodeURIComponent(projectPath[2])}`,
109
- );
110
- return project;
111
- };
110
+ export const getProjectInfo = async (vorpal: CommandInstance | null) =>
111
+ getGitlabProjectInfo();
112
112
 
113
113
  export type GitlabVariable = {
114
114
  variable_type: string;
@@ -121,25 +121,36 @@ export type GitlabVariable = {
121
121
  export const getAllVariables = memoizee(
122
122
  async (
123
123
  vorpal: CommandInstance | null,
124
- n = 5, // how many requests to do in parallel, 5 seems a good value for many projects, ideally we would do one request in parallel per component
125
- ): Promise<Array<GitlabVariable>> => {
126
- const { id } = await getProjectInfo(vorpal);
127
-
128
- let all: Array<GitlabVariable> = [];
129
- let result: Array<Array<GitlabVariable>> = [];
124
+ preinit?: [number, GitlabRestClient],
125
+ ): Promise<(ProjectVariableSchema | Camelize<ProjectVariableSchema>)[]> => {
126
+ const [id, gitlabApi] =
127
+ preinit ?? (await Promise.all([getGitlabProjectId(), gitlabApiClient()]));
128
+ // how many requests to do in parallel, 5 seems a good value for many projects, ideally we would do one request in parallel per component
129
+ const n = 5;
130
+ let all: (ProjectVariableSchema | Camelize<ProjectVariableSchema>)[] = [];
131
+ let result: (ProjectVariableSchema | Camelize<ProjectVariableSchema>)[][] =
132
+ [];
130
133
  let page = 1;
131
134
 
132
135
  do {
133
136
  // Create an array of promises for N pages
134
137
  const promises = Array.from({ length: n }, (_, i) => {
135
- return doGitlabRequest(
136
- vorpal,
137
- `projects/${id}/variables?per_page=100&page=${page + i}`,
138
- );
138
+ return gitlabApi.ProjectVariables.all(id, {
139
+ page: page + i,
140
+ perPage: 100,
141
+ });
139
142
  });
140
143
 
141
144
  // Wait for all promises to resolve
142
- result = await Promise.all(promises);
145
+ result = await Promise.all(
146
+ Array.from({ length: n }, async (_, i) => {
147
+ const res = await gitlabApi.ProjectVariables.all(id, {
148
+ page: page + i,
149
+ perPage: 100,
150
+ });
151
+ return res;
152
+ }),
153
+ );
143
154
 
144
155
  // Increment the page by N
145
156
  page += n;
@@ -165,60 +176,11 @@ export const getVariableValueByRawName = async (
165
176
  const maskableRegex = new RegExp("^[a-zA-Z0-9_+=/@:.~-]{8,}$"); // SEE https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js#L20
166
177
  const isMaskable = (value: string): boolean => maskableRegex.test(value);
167
178
 
168
- const createVariable = async (
169
- vorpal: CommandInstance,
170
- projectId: string,
171
- key: string,
172
- value: string,
173
- masked = true,
174
- environment_scope = "*",
175
- ) => {
176
- return await doGitlabRequest(
177
- vorpal,
178
- `projects/${projectId}/variables`,
179
- {
180
- key,
181
- value,
182
- masked: masked && isMaskable(value),
183
- environment_scope,
184
- },
185
- "POST",
186
- );
187
- };
188
-
189
- const updateVariable = async (
179
+ const getAllCatladderEnvVarsInGitlab = async (
190
180
  vorpal: CommandInstance,
191
- projectId: string,
192
- key: string,
193
- value: string,
194
- masked = true,
181
+ preinit?: [number, GitlabRestClient],
195
182
  ) => {
196
- return await doGitlabRequest(
197
- vorpal,
198
- `projects/${projectId}/variables/${key}`,
199
- {
200
- value,
201
- masked: masked && isMaskable(value),
202
- },
203
- "PUT",
204
- );
205
- };
206
-
207
- const deleteVariable = async (
208
- vorpal: CommandInstance,
209
- projectId: string,
210
- key: string,
211
- ) => {
212
- return await doGitlabRequest(
213
- vorpal,
214
- `projects/${projectId}/variables/${key}`,
215
- undefined,
216
- "DELETE",
217
- );
218
- };
219
-
220
- const getAllCatladderEnvVarsInGitlab = async (vorpal: CommandInstance) => {
221
- const allVariables = await getAllVariables(vorpal).then((v) =>
183
+ const allVariables = await getAllVariables(vorpal, preinit).then((v) =>
222
184
  v.reduce<{
223
185
  [key: string]: {
224
186
  value?: string;
@@ -263,15 +225,23 @@ const getBackupKey = (fullKey: string, timestamp: number) =>
263
225
  `${fullKey}_backup_${timestamp}`;
264
226
 
265
227
  export const clearBackups = async (vorpal: CommandInstance, keep: number) => {
266
- const existingVariables = await getAllCatladderEnvVarsInGitlab(vorpal);
267
- const { id } = await getProjectInfo(vorpal);
228
+ const [id, gitlabApi] = await Promise.all([
229
+ getGitlabProjectId(),
230
+ gitlabApiClient(),
231
+ ]);
232
+ const existingVariables = await getAllCatladderEnvVarsInGitlab(vorpal, [
233
+ id,
234
+ gitlabApi,
235
+ ]);
236
+ // const { id } = await getProjectInfo(vorpal);
268
237
  for (const [key, { backups }] of Object.entries(existingVariables)) {
269
238
  const backupsSorted = backups.sort((a, b) => b - a);
270
239
  //const toKeep = backupsSorted.slice(0, keep);
271
240
  const toDelete = backupsSorted.slice(keep);
272
241
 
273
242
  for (const timestamp of toDelete) {
274
- await deleteVariable(vorpal, id, getBackupKey(key, timestamp));
243
+ await gitlabApi.ProjectVariables.remove(id, getBackupKey(key, timestamp));
244
+ // await deleteVariable(vorpal, id, getBackupKey(key, timestamp));
275
245
  }
276
246
  }
277
247
  };
@@ -281,13 +251,26 @@ export const upsertAllVariables = async (
281
251
  variables: Record<string, any>,
282
252
  env: string,
283
253
  componentName: string,
284
- backup = true,
285
- masked = true, // FIXME: would be better to have this per variable
254
+ {
255
+ backup = true,
256
+ masked = true,
257
+ preinit,
258
+ }: {
259
+ backup?: boolean;
260
+ // FIXME: would be better to have this per variable
261
+ masked?: boolean;
262
+ preinit?: [number, GitlabRestClient];
263
+ } = {},
286
264
  ): Promise<void> => {
287
- const { id } = await getProjectInfo(vorpal);
265
+ const [id, gitlabApi] =
266
+ preinit ?? (await Promise.all([getGitlabProjectId(), gitlabApiClient()]));
267
+ // const { id } = await getProjectInfo(vorpal);
288
268
 
289
269
  // we list all existing variables. We also gather backup versions, but its currently unused
290
- const existingVariables = await getAllCatladderEnvVarsInGitlab(vorpal);
270
+ const existingVariables = await getAllCatladderEnvVarsInGitlab(vorpal, [
271
+ id,
272
+ gitlabApi,
273
+ ]);
291
274
  for (const [key, value] of Object.entries(variables ?? {})) {
292
275
  const fullKey = getSecretVarName(env, componentName, key);
293
276
  const valueSanitized = isObject(value) ? JSON.stringify(value) : `${value}`;
@@ -299,21 +282,30 @@ export const upsertAllVariables = async (
299
282
  if (exists) {
300
283
  vorpal.log(`changed: ${key}`);
301
284
 
302
- await updateVariable(vorpal, id, fullKey, valueSanitized, masked);
285
+ await gitlabApi.ProjectVariables.edit(id, fullKey, valueSanitized, {
286
+ filter: { environment_scope: "All (default)" },
287
+ masked,
288
+ });
289
+ // await updateVariable(vorpal, id, fullKey, valueSanitized, masked);
303
290
  // write backup
304
291
  if (backup) {
305
- await createVariable(
306
- vorpal,
292
+ await gitlabApi.ProjectVariables.create(
307
293
  id,
308
294
  getBackupKey(fullKey, new Date().getTime()),
309
295
  oldValue,
310
- masked,
311
- "_backup",
296
+ {
297
+ environmentScope: "_backup",
298
+ masked,
299
+ },
312
300
  );
313
301
  }
314
302
  } else {
315
303
  vorpal.log(`new : ${key}`);
316
- await createVariable(vorpal, id, fullKey, valueSanitized, masked);
304
+ await gitlabApi.ProjectVariables.create(id, fullKey, valueSanitized, {
305
+ environmentScope: "All (default)",
306
+ masked,
307
+ });
308
+ // await createVariable(vorpal, id, fullKey, valueSanitized, masked);
317
309
  }
318
310
  } else {
319
311
  vorpal.log(`skip : ${key}`);
@@ -1,9 +1,11 @@
1
- import { createFile, pathExists, readFile, writeFile } from "fs-extra";
1
+ import { writeFile, readFile, mkdir } from "fs/promises";
2
+ import { existsSync } from "fs";
2
3
  import { homedir } from "os";
3
-
4
4
  import { parse, stringify } from "yaml";
5
- const directory = `${homedir()}/.catladder`;
6
- const file = `${directory}/preferences.yml`;
5
+ import { join } from "path";
6
+
7
+ const directory = join(homedir(), ".catladder");
8
+ const file = join(directory, "preferences.yml");
7
9
 
8
10
  export type CatladderCliPreferences = {
9
11
  "gitlab-personal-access-token": string | null;
@@ -15,7 +17,7 @@ const perfKeys = [
15
17
  "gitlab-personal-access-token",
16
18
  "googleAuthUserNumber",
17
19
  "gitlabTokens",
18
- ] as const;
20
+ ] as const satisfies readonly PreferenceKey[];
19
21
 
20
22
  const fallbackValues = {
21
23
  "gitlab-personal-access-token": null,
@@ -23,23 +25,47 @@ const fallbackValues = {
23
25
  googleAuthUserNumber: null,
24
26
  } satisfies CatladderCliPreferences;
25
27
 
26
- export const getPreferences = async (): Promise<CatladderCliPreferences> => {
27
- if (!(await pathExists(file))) {
28
- await createFile(file);
29
- }
30
- const fileString = await readFile(file, { encoding: "utf-8" });
31
- const fileParsed = parse(fileString || "---\nnull") ?? {};
32
- return perfKeys.reduce(
28
+ const perferencesWithParsed = (
29
+ parsed: Partial<CatladderCliPreferences>,
30
+ ): CatladderCliPreferences =>
31
+ perfKeys.reduce(
33
32
  (acc, key) =>
34
- Object.assign(acc, { [key]: fileParsed[key] ?? fallbackValues[key] }),
33
+ Object.assign(acc, { [key]: parsed[key] ?? fallbackValues[key] }),
35
34
  {} as CatladderCliPreferences,
36
35
  );
36
+
37
+ const encoding = "utf-8";
38
+
39
+ const readPreferences = () => readFile(file, { encoding });
40
+
41
+ const writePreferences = (preferences: CatladderCliPreferences) =>
42
+ writeFile(file, stringify(preferences), { encoding, mode: 0o600 });
43
+
44
+ const ensureDir = async () =>
45
+ existsSync(directory)
46
+ ? directory
47
+ : mkdir(directory, { recursive: true, mode: 0o700 });
48
+
49
+ const ensureFile = async () => {
50
+ if (existsSync(file)) {
51
+ return file;
52
+ }
53
+ await ensureDir();
54
+ return writePreferences(perferencesWithParsed({}));
55
+ };
56
+
57
+ export const getPreferences = async (): Promise<CatladderCliPreferences> => {
58
+ await ensureFile();
59
+ const fileString = await readPreferences();
60
+ const fileParsed = parse(fileString || "---\nnull") ?? {};
61
+ return perferencesWithParsed(fileParsed);
37
62
  };
38
63
 
39
64
  export const hasPreference = async (key: PreferenceKey) => {
40
65
  const preferences = await getPreferences();
41
66
  return preferences[key] !== null && preferences[key] !== undefined;
42
67
  };
68
+
43
69
  export const getPreference = async <K extends PreferenceKey>(
44
70
  key: K,
45
71
  ): Promise<CatladderCliPreferences[K] | null> => {
@@ -55,11 +81,5 @@ export const setPreference = async (
55
81
  value: CatladderCliPreferences[typeof key],
56
82
  ) => {
57
83
  const preferences = await getPreferences();
58
-
59
- const newPreferences = {
60
- ...preferences,
61
- [key]: value,
62
- };
63
-
64
- await writeFile(file, stringify(newPreferences));
84
+ return writePreferences({ ...preferences, [key]: value });
65
85
  };
@@ -0,0 +1,78 @@
1
+ import { writeFile, readFile, mkdir } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { getProjectRootPath } from "../git";
5
+
6
+ const directory = ".catladder";
7
+ const file = "preferences.json";
8
+
9
+ export type CatladderCliProjectPreferences = {
10
+ gitlabProjectId?: number | null;
11
+ };
12
+ type ProjectPreferenceKey = keyof CatladderCliProjectPreferences;
13
+ const perfKeys = [
14
+ "gitlabProjectId",
15
+ ] as const satisfies readonly ProjectPreferenceKey[];
16
+ const fallbackValues = {
17
+ gitlabProjectId: null,
18
+ } satisfies CatladderCliProjectPreferences;
19
+
20
+ const perferencesWithParsed = (
21
+ parsed: Partial<CatladderCliProjectPreferences>,
22
+ ): CatladderCliProjectPreferences =>
23
+ perfKeys.reduce(
24
+ (acc, key) =>
25
+ Object.assign(acc, { [key]: parsed[key] ?? fallbackValues[key] ?? null }),
26
+ {} as CatladderCliProjectPreferences,
27
+ );
28
+
29
+ const encoding = "utf-8";
30
+
31
+ const readPreferences = (filePath: string) => readFile(filePath, { encoding });
32
+
33
+ const writePreferences = (
34
+ filePath: string,
35
+ preferences: CatladderCliProjectPreferences,
36
+ ) =>
37
+ writeFile(filePath, JSON.stringify(preferences, null, 2), {
38
+ encoding,
39
+ mode: 0o600,
40
+ });
41
+
42
+ const ensureDir = async (dirPath: string) =>
43
+ existsSync(dirPath)
44
+ ? dirPath
45
+ : mkdir(dirPath, { recursive: true, mode: 0o700 });
46
+
47
+ const ensureFile = async (givenFilePath?: string) => {
48
+ if (givenFilePath?.length && existsSync(givenFilePath)) {
49
+ return givenFilePath;
50
+ }
51
+ const rootPath = await getProjectRootPath();
52
+ const dirPath = join(rootPath, directory);
53
+ const filePath = join(dirPath, file);
54
+ if (existsSync(filePath)) {
55
+ return filePath;
56
+ }
57
+ await ensureDir(dirPath);
58
+ await writePreferences(filePath, perferencesWithParsed({}));
59
+ return filePath;
60
+ };
61
+
62
+ export const getProjectPreferences = async (
63
+ givenFilePath?: string,
64
+ ): Promise<CatladderCliProjectPreferences> => {
65
+ const filePath = await ensureFile(givenFilePath);
66
+ const fileString = await readPreferences(filePath);
67
+ const fileParsed = JSON.parse(fileString || "null") ?? {};
68
+ return perferencesWithParsed(fileParsed);
69
+ };
70
+
71
+ export const upsertProjectPerferences = async (
72
+ newPerfs: Partial<CatladderCliProjectPreferences> = {},
73
+ givenFilePath?: string,
74
+ ) => {
75
+ const filePath = await ensureFile(givenFilePath);
76
+ const currentPerfs = await getProjectPreferences(filePath);
77
+ return writePreferences(filePath, { ...currentPerfs, ...newPerfs });
78
+ };