@credibledata/cred-cli 0.0.100 → 0.0.103

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 (225) hide show
  1. package/README-DEV.md +2 -2
  2. package/README.md +1 -1
  3. package/coverage/coverage-summary.json +40 -0
  4. package/coverage/tmp/coverage-2152-1789496974457-0.json +1 -0
  5. package/coverage/tmp/coverage-2153-1789496954404-0.json +1 -0
  6. package/dist/client/api.js +6151 -2254
  7. package/dist/client/api.js.map +1 -1
  8. package/dist/client/base.js +1 -1
  9. package/dist/client/common.js +1 -1
  10. package/dist/client/configuration.js +1 -1
  11. package/dist/client/index.js +1 -1
  12. package/dist/clientconfiguration.js +114 -26
  13. package/dist/clientconfiguration.js.map +1 -1
  14. package/dist/clientconfiguration.spec.js +159 -37
  15. package/dist/clientconfiguration.spec.js.map +1 -1
  16. package/dist/commands/add.js +36 -5
  17. package/dist/commands/add.js.map +1 -1
  18. package/dist/commands/connection/createconnections.js +4 -4
  19. package/dist/commands/connection/createconnections.js.map +1 -1
  20. package/dist/commands/connection/createconnections.spec.js +11 -1
  21. package/dist/commands/connection/createconnections.spec.js.map +1 -1
  22. package/dist/commands/connection/deleteconnection.js +4 -4
  23. package/dist/commands/connection/deleteconnection.js.map +1 -1
  24. package/dist/commands/connection/deleteconnection.spec.js +1 -1
  25. package/dist/commands/connection/deleteconnection.spec.js.map +1 -1
  26. package/dist/commands/connection/listconnections.js +4 -4
  27. package/dist/commands/connection/listconnections.js.map +1 -1
  28. package/dist/commands/connection/listconnections.spec.js +1 -1
  29. package/dist/commands/connection/listconnections.spec.js.map +1 -1
  30. package/dist/commands/{project/createproject.js → environment/createenvironment.js} +17 -14
  31. package/dist/commands/environment/createenvironment.js.map +1 -0
  32. package/dist/commands/{project/createproject.spec.js → environment/createenvironment.spec.js} +46 -46
  33. package/dist/commands/environment/createenvironment.spec.js.map +1 -0
  34. package/dist/commands/{project/deleteproject.js → environment/deleteenvironment.js} +9 -9
  35. package/dist/commands/environment/deleteenvironment.js.map +1 -0
  36. package/dist/commands/{project/deleteproject.spec.js → environment/deleteenvironment.spec.js} +29 -29
  37. package/dist/commands/environment/deleteenvironment.spec.js.map +1 -0
  38. package/dist/commands/{project/getproject.js → environment/getenvironment.js} +8 -8
  39. package/dist/commands/environment/getenvironment.js.map +1 -0
  40. package/dist/commands/{project/getproject.spec.js → environment/getenvironment.spec.js} +32 -33
  41. package/dist/commands/environment/getenvironment.spec.js.map +1 -0
  42. package/dist/commands/{project/listprojects.js → environment/listenvironments.js} +10 -10
  43. package/dist/commands/environment/listenvironments.js.map +1 -0
  44. package/dist/commands/{project/listprojects.spec.js → environment/listenvironments.spec.js} +35 -35
  45. package/dist/commands/environment/listenvironments.spec.js.map +1 -0
  46. package/dist/commands/{project/setproject.js → environment/setenvironment.js} +6 -5
  47. package/dist/commands/environment/setenvironment.js.map +1 -0
  48. package/dist/commands/{project/setproject.spec.js → environment/setenvironment.spec.js} +9 -9
  49. package/dist/commands/environment/setenvironment.spec.js.map +1 -0
  50. package/dist/commands/environment/updateenvironment.js +130 -0
  51. package/dist/commands/environment/updateenvironment.js.map +1 -0
  52. package/dist/commands/environment/updateenvironment.spec.js +213 -0
  53. package/dist/commands/environment/updateenvironment.spec.js.map +1 -0
  54. package/dist/commands/get.js +66 -5
  55. package/dist/commands/get.js.map +1 -1
  56. package/dist/commands/legacy.js +9 -0
  57. package/dist/commands/legacy.js.map +1 -0
  58. package/dist/commands/list.js +95 -4
  59. package/dist/commands/list.js.map +1 -1
  60. package/dist/commands/materialization/cancelrun.js +62 -0
  61. package/dist/commands/materialization/cancelrun.js.map +1 -0
  62. package/dist/commands/materialization/getindex.js +52 -0
  63. package/dist/commands/materialization/getindex.js.map +1 -0
  64. package/dist/commands/materialization/getmaterialization.js +54 -0
  65. package/dist/commands/materialization/getmaterialization.js.map +1 -0
  66. package/dist/commands/materialization/getpreaggregate.js +55 -0
  67. package/dist/commands/materialization/getpreaggregate.js.map +1 -0
  68. package/dist/commands/materialization/getrun.js +54 -0
  69. package/dist/commands/materialization/getrun.js.map +1 -0
  70. package/dist/commands/materialization/listindexes.js +58 -0
  71. package/dist/commands/materialization/listindexes.js.map +1 -0
  72. package/dist/commands/materialization/listmaterializations.js +52 -0
  73. package/dist/commands/materialization/listmaterializations.js.map +1 -0
  74. package/dist/commands/materialization/listpreaggregates.js +61 -0
  75. package/dist/commands/materialization/listpreaggregates.js.map +1 -0
  76. package/dist/commands/materialization/listruns.js +60 -0
  77. package/dist/commands/materialization/listruns.js.map +1 -0
  78. package/dist/commands/materialization/rerun.js +111 -0
  79. package/dist/commands/materialization/rerun.js.map +1 -0
  80. package/dist/commands/misc/login.js +16 -2
  81. package/dist/commands/misc/login.js.map +1 -1
  82. package/dist/commands/misc/login.spec.js +133 -5
  83. package/dist/commands/misc/login.spec.js.map +1 -1
  84. package/dist/commands/package/deletepackage.js +4 -4
  85. package/dist/commands/package/deletepackage.js.map +1 -1
  86. package/dist/commands/package/deletepackage.spec.js +1 -1
  87. package/dist/commands/package/deletepackage.spec.js.map +1 -1
  88. package/dist/commands/package/listpackages.js +11 -4
  89. package/dist/commands/package/listpackages.js.map +1 -1
  90. package/dist/commands/package/listpackages.spec.js +13 -1
  91. package/dist/commands/package/listpackages.spec.js.map +1 -1
  92. package/dist/commands/package/publish.js +36 -13
  93. package/dist/commands/package/publish.js.map +1 -1
  94. package/dist/commands/package/publish.spec.js +27 -1
  95. package/dist/commands/package/publish.spec.js.map +1 -1
  96. package/dist/commands/package/updatepackage.js +4 -4
  97. package/dist/commands/package/updatepackage.js.map +1 -1
  98. package/dist/commands/package/updatepackage.spec.js +1 -1
  99. package/dist/commands/package/updatepackage.spec.js.map +1 -1
  100. package/dist/commands/permission/createpermission.js +123 -0
  101. package/dist/commands/permission/createpermission.js.map +1 -0
  102. package/dist/commands/permission/createpermission.spec.js +360 -0
  103. package/dist/commands/permission/createpermission.spec.js.map +1 -0
  104. package/dist/commands/permission/deletepermission.js +86 -0
  105. package/dist/commands/permission/deletepermission.js.map +1 -0
  106. package/dist/commands/permission/getpermission.js +85 -0
  107. package/dist/commands/permission/getpermission.js.map +1 -0
  108. package/dist/commands/permission/listpermissions.js +75 -0
  109. package/dist/commands/permission/listpermissions.js.map +1 -0
  110. package/dist/commands/permission/scope.js +140 -0
  111. package/dist/commands/permission/scope.js.map +1 -0
  112. package/dist/commands/rm.js +26 -4
  113. package/dist/commands/rm.js.map +1 -1
  114. package/dist/commands/set.js +12 -4
  115. package/dist/commands/set.js.map +1 -1
  116. package/dist/commands/update.js +29 -10
  117. package/dist/commands/update.js.map +1 -1
  118. package/dist/commands/version/archive-unarchive-version.js +4 -5
  119. package/dist/commands/version/archive-unarchive-version.js.map +1 -1
  120. package/dist/commands/version/archive-unarchive-version.spec.js +4 -4
  121. package/dist/commands/version/archive-unarchive-version.spec.js.map +1 -1
  122. package/dist/commands/version/listversions.js +4 -4
  123. package/dist/commands/version/listversions.js.map +1 -1
  124. package/dist/commands/version/listversions.spec.js +1 -1
  125. package/dist/commands/version/listversions.spec.js.map +1 -1
  126. package/dist/common/UserGroupId.js +1 -1
  127. package/dist/common/UserGroupId.js.map +1 -1
  128. package/dist/common/UserGroupId.spec.js +2 -4
  129. package/dist/common/UserGroupId.spec.js.map +1 -1
  130. package/dist/constants.js +95 -19
  131. package/dist/constants.js.map +1 -1
  132. package/dist/constants.spec.js +90 -0
  133. package/dist/constants.spec.js.map +1 -0
  134. package/dist/index.js +128 -15
  135. package/dist/index.js.map +1 -1
  136. package/dist/logger.js +5 -1
  137. package/dist/logger.js.map +1 -1
  138. package/openapitools.json +4 -1
  139. package/package.json +10 -9
  140. package/src/client/api.ts +9363 -3093
  141. package/src/client/base.ts +1 -1
  142. package/src/client/common.ts +1 -1
  143. package/src/client/configuration.ts +1 -1
  144. package/src/client/index.ts +1 -1
  145. package/src/clientconfiguration.spec.ts +238 -113
  146. package/src/clientconfiguration.ts +131 -30
  147. package/src/commands/add.ts +78 -12
  148. package/src/commands/connection/createconnections.spec.ts +11 -1
  149. package/src/commands/connection/createconnections.ts +4 -4
  150. package/src/commands/connection/deleteconnection.spec.ts +1 -1
  151. package/src/commands/connection/deleteconnection.ts +4 -4
  152. package/src/commands/connection/listconnections.spec.ts +1 -1
  153. package/src/commands/connection/listconnections.ts +4 -4
  154. package/src/commands/{project/createproject.spec.ts → environment/createenvironment.spec.ts} +56 -54
  155. package/src/commands/{project/createproject.ts → environment/createenvironment.ts} +17 -14
  156. package/src/commands/{project/deleteproject.spec.ts → environment/deleteenvironment.spec.ts} +59 -30
  157. package/src/commands/{project/deleteproject.ts → environment/deleteenvironment.ts} +10 -10
  158. package/src/commands/environment/getenvironment.spec.ts +110 -0
  159. package/src/commands/{project/getproject.ts → environment/getenvironment.ts} +9 -9
  160. package/src/commands/{project/listprojects.spec.ts → environment/listenvironments.spec.ts} +37 -35
  161. package/src/commands/{project/listprojects.ts → environment/listenvironments.ts} +13 -8
  162. package/src/commands/{project/setproject.spec.ts → environment/setenvironment.spec.ts} +8 -8
  163. package/src/commands/{project/setproject.ts → environment/setenvironment.ts} +4 -4
  164. package/src/commands/environment/updateenvironment.spec.ts +386 -0
  165. package/src/commands/environment/updateenvironment.ts +168 -0
  166. package/src/commands/get.ts +131 -9
  167. package/src/commands/legacy.ts +5 -0
  168. package/src/commands/list.ts +204 -9
  169. package/src/commands/materialization/cancelrun.ts +70 -0
  170. package/src/commands/materialization/getindex.ts +55 -0
  171. package/src/commands/materialization/getmaterialization.ts +57 -0
  172. package/src/commands/materialization/getpreaggregate.ts +58 -0
  173. package/src/commands/materialization/getrun.ts +55 -0
  174. package/src/commands/materialization/listindexes.ts +69 -0
  175. package/src/commands/materialization/listmaterializations.ts +53 -0
  176. package/src/commands/materialization/listpreaggregates.ts +74 -0
  177. package/src/commands/materialization/listruns.ts +77 -0
  178. package/src/commands/materialization/rerun.ts +123 -0
  179. package/src/commands/misc/login.spec.ts +165 -7
  180. package/src/commands/misc/login.ts +20 -2
  181. package/src/commands/package/deletepackage.spec.ts +1 -1
  182. package/src/commands/package/deletepackage.ts +4 -4
  183. package/src/commands/package/listpackages.spec.ts +17 -1
  184. package/src/commands/package/listpackages.ts +11 -4
  185. package/src/commands/package/publish.spec.ts +46 -1
  186. package/src/commands/package/publish.ts +38 -13
  187. package/src/commands/package/updatepackage.spec.ts +1 -1
  188. package/src/commands/package/updatepackage.ts +4 -4
  189. package/src/commands/permission/createpermission.spec.ts +507 -0
  190. package/src/commands/permission/createpermission.ts +181 -0
  191. package/src/commands/permission/deletepermission.ts +117 -0
  192. package/src/commands/permission/getpermission.ts +115 -0
  193. package/src/commands/permission/listpermissions.ts +101 -0
  194. package/src/commands/permission/scope.ts +180 -0
  195. package/src/commands/rm.ts +54 -9
  196. package/src/commands/set.ts +13 -4
  197. package/src/commands/update.ts +66 -23
  198. package/src/commands/version/archive-unarchive-version.spec.ts +4 -4
  199. package/src/commands/version/archive-unarchive-version.ts +4 -7
  200. package/src/commands/version/listversions.spec.ts +1 -1
  201. package/src/commands/version/listversions.ts +4 -4
  202. package/src/common/UserGroupId.spec.ts +2 -4
  203. package/src/common/UserGroupId.ts +1 -1
  204. package/src/constants.spec.ts +111 -0
  205. package/src/constants.ts +131 -19
  206. package/src/index.ts +187 -18
  207. package/src/logger.ts +5 -1
  208. package/bun.lock +0 -1158
  209. package/dist/commands/project/createproject.js.map +0 -1
  210. package/dist/commands/project/createproject.spec.js.map +0 -1
  211. package/dist/commands/project/deleteproject.js.map +0 -1
  212. package/dist/commands/project/deleteproject.spec.js.map +0 -1
  213. package/dist/commands/project/getproject.js.map +0 -1
  214. package/dist/commands/project/getproject.spec.js.map +0 -1
  215. package/dist/commands/project/listprojects.js.map +0 -1
  216. package/dist/commands/project/listprojects.spec.js.map +0 -1
  217. package/dist/commands/project/setproject.js.map +0 -1
  218. package/dist/commands/project/setproject.spec.js.map +0 -1
  219. package/dist/commands/project/updateproject.js +0 -68
  220. package/dist/commands/project/updateproject.js.map +0 -1
  221. package/dist/commands/project/updateproject.spec.js +0 -129
  222. package/dist/commands/project/updateproject.spec.js.map +0 -1
  223. package/src/commands/project/getproject.spec.ts +0 -109
  224. package/src/commands/project/updateproject.spec.ts +0 -217
  225. package/src/commands/project/updateproject.ts +0 -82
@@ -1,8 +1,14 @@
1
+ import crypto from "crypto";
1
2
  import fs from "fs";
2
3
  import { jwtDecode } from "jwt-decode";
3
4
  import kleur from "kleur";
4
5
  import os from "os";
5
- import { ALL_ENVIRONMENTS, ASCII_TITLE, Environment } from "./constants";
6
+ import {
7
+ ALL_ENVIRONMENTS,
8
+ ASCII_TITLE,
9
+ controlPlanesByEnv,
10
+ Environment,
11
+ } from "./constants";
6
12
  import { logger } from "./logger";
7
13
 
8
14
  interface ConfigFile {
@@ -33,6 +39,15 @@ interface JwtPayload {
33
39
  sub?: string;
34
40
  }
35
41
 
42
+ /**
43
+ * Thrown when `.cred` exists but cannot be used: either it does not parse as
44
+ * JSON or it parses but lacks the required top-level shape. Distinct from fs
45
+ * errors (ENOENT, EACCES) so callers that want to recover — `cred login` —
46
+ * can target *only* a corrupt-content failure and leave permission/IO errors
47
+ * to surface loudly.
48
+ */
49
+ export class InvalidConfigError extends Error {}
50
+
36
51
  export class ClientConfiguration {
37
52
  private configPath = `${os.homedir()}/.cred`;
38
53
  private config: ConfigFile;
@@ -44,28 +59,35 @@ export class ClientConfiguration {
44
59
  }
45
60
 
46
61
  public loadConfig(): ConfigFile {
62
+ let fileContents: Buffer | undefined;
47
63
  try {
48
- const fileContents = fs.readFileSync(this.configPath);
49
- this.config = JSON.parse(fileContents.toString());
50
- if (
51
- !this.config.activeEnvironment ||
52
- !this.config.configPerEnvironment
53
- ) {
54
- throw new Error(
55
- "Corrupted config file found, restoring to default settings.",
56
- );
57
- }
64
+ fileContents = fs.readFileSync(this.configPath);
58
65
  } catch (error) {
59
- logger.warn(error);
60
- const configPerEnvironment: ConfigFile["configPerEnvironment"] = {};
61
- for (const env of ALL_ENVIRONMENTS) {
62
- configPerEnvironment[env] = {};
66
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
67
+ // First-time setup: create a default config on disk.
68
+ this.config = this.defaultConfig();
69
+ this.writeConfig();
70
+ return this.config;
63
71
  }
64
- this.config = {
65
- configPerEnvironment,
66
- activeEnvironment: "production",
67
- };
68
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
72
+ throw error;
73
+ }
74
+
75
+ try {
76
+ this.config = JSON.parse(fileContents.toString());
77
+ } catch (error) {
78
+ // Non-atomic writes from older binaries, or a manually corrupted file,
79
+ // can leave `.cred` unparseable. Fail loud rather than silently
80
+ // overwriting — a silent reset erases the access token for every
81
+ // subsequent caller on the same machine.
82
+ throw new InvalidConfigError(
83
+ `Failed to parse ${this.configPath}: ${error}. The file may be corrupt; re-run \`cred login\` to reset.`,
84
+ );
85
+ }
86
+
87
+ if (!this.config.activeEnvironment || !this.config.configPerEnvironment) {
88
+ throw new InvalidConfigError(
89
+ `Invalid config structure in ${this.configPath}; re-run \`cred login\` to reset.`,
90
+ );
69
91
  }
70
92
 
71
93
  for (const env of ALL_ENVIRONMENTS) {
@@ -77,13 +99,86 @@ export class ClientConfiguration {
77
99
  return this.config;
78
100
  }
79
101
 
102
+ /**
103
+ * Ensure `.cred` is usable, recovering from a corrupt file if needed.
104
+ *
105
+ * `loadConfig` deliberately fails loud on a corrupt/invalid file so a read
106
+ * path never silently erases tokens. But `cred login` exists precisely to
107
+ * (re)establish config, so it must be able to get past a bad file — that is
108
+ * what the "re-run `cred login` to reset" message promises. We make the
109
+ * message true here: the bad file is renamed aside (never deleted, so a
110
+ * support engineer can still inspect it) and a fresh default is written.
111
+ *
112
+ * Only an {@link InvalidConfigError} triggers recovery. fs errors such as
113
+ * EACCES are real environment problems and are rethrown — resetting would
114
+ * just fail to write anyway, and we must not mask a permissions issue as a
115
+ * successful reset.
116
+ *
117
+ * @returns the path the corrupt file was backed up to, or null if the
118
+ * existing config was already valid.
119
+ */
120
+ public recoverIfCorrupt(): string | null {
121
+ try {
122
+ this.loadConfig();
123
+ return null;
124
+ } catch (error) {
125
+ if (!(error instanceof InvalidConfigError)) {
126
+ throw error;
127
+ }
128
+ const backupPath = `${this.configPath}.corrupt-${crypto
129
+ .randomBytes(8)
130
+ .toString("hex")}`;
131
+ fs.renameSync(this.configPath, backupPath);
132
+ logger.warn(
133
+ `Existing config at ${this.configPath} was invalid and could not be read. ` +
134
+ `Backed it up to ${backupPath} and reset to defaults.`,
135
+ );
136
+ this.config = this.defaultConfig();
137
+ this.writeConfig();
138
+ return backupPath;
139
+ }
140
+ }
141
+
142
+ private defaultConfig(): ConfigFile {
143
+ const configPerEnvironment: ConfigFile["configPerEnvironment"] = {};
144
+ for (const env of ALL_ENVIRONMENTS) {
145
+ configPerEnvironment[env] = {};
146
+ }
147
+ return { configPerEnvironment, activeEnvironment: "production" };
148
+ }
149
+
150
+ // Atomic: write to a sibling temp file, then rename into place.
151
+ // Readers either see the old file or the new file, never a truncated
152
+ // zero-byte snapshot (the window that `writeFileSync` exposes between
153
+ // open(O_TRUNC) and write).
154
+ //
155
+ // The temp suffix uses 16 bytes of crypto-random hex so the temp
156
+ // path is unguessable — a predictable name (pid + Date.now()) lets a
157
+ // co-located attacker pre-create a symlink at that path and trick
158
+ // us into clobbering an arbitrary file (CodeQL js/insecure-temporary-file).
159
+ // O_EXCL on open further refuses to follow a pre-existing entry.
160
+ private writeConfig(): void {
161
+ const tmp = `${this.configPath}.${crypto.randomBytes(16).toString("hex")}.tmp`;
162
+ const fd = fs.openSync(
163
+ tmp,
164
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
165
+ 0o600,
166
+ );
167
+ try {
168
+ fs.writeFileSync(fd, JSON.stringify(this.config));
169
+ } finally {
170
+ fs.closeSync(fd);
171
+ }
172
+ fs.renameSync(tmp, this.configPath);
173
+ }
174
+
80
175
  public setOrganization(organizationName: string) {
81
176
  this.loadConfig();
82
177
  // TODO: Validate organization exists and user has access.
83
178
  const environment = this.getEnvironment();
84
179
  this.config.configPerEnvironment[environment].organization =
85
180
  organizationName;
86
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
181
+ this.writeConfig();
87
182
  }
88
183
 
89
184
  public setProject(projectName: string) {
@@ -91,7 +186,7 @@ export class ClientConfiguration {
91
186
  // TODO: Validate project exists and user has access.
92
187
  const environment = this.getEnvironment();
93
188
  this.config.configPerEnvironment[environment].project = projectName;
94
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
189
+ this.writeConfig();
95
190
  }
96
191
 
97
192
  public getOrganization() {
@@ -112,7 +207,7 @@ export class ClientConfiguration {
112
207
  this.config.configPerEnvironment[environment].isServiceAccountToken =
113
208
  !userToken;
114
209
  this.config.configPerEnvironment[environment].accessToken = accessToken;
115
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
210
+ this.writeConfig();
116
211
  }
117
212
 
118
213
  public getAuthenticationHeader() {
@@ -135,7 +230,7 @@ export class ClientConfiguration {
135
230
  const environment = this.getEnvironment();
136
231
  this.config.configPerEnvironment[environment].jwtAccessToken =
137
232
  jwtAccessToken;
138
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
233
+ this.writeConfig();
139
234
  }
140
235
 
141
236
  public getJwtAccessToken(): string | undefined {
@@ -154,7 +249,7 @@ export class ClientConfiguration {
154
249
  username,
155
250
  email,
156
251
  };
157
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
252
+ this.writeConfig();
158
253
  }
159
254
 
160
255
  public getVSCodeUser(): { username: string; email: string } | undefined {
@@ -167,7 +262,7 @@ export class ClientConfiguration {
167
262
  this.loadConfig();
168
263
  const environment = this.getEnvironment();
169
264
  this.config.configPerEnvironment[environment].refreshToken = refreshToken;
170
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
265
+ this.writeConfig();
171
266
  }
172
267
 
173
268
  public getRefreshToken(): string | undefined {
@@ -192,13 +287,13 @@ export class ClientConfiguration {
192
287
  this.loadConfig();
193
288
  const environment = this.getEnvironment();
194
289
  this.config.configPerEnvironment[environment] = {};
195
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
290
+ this.writeConfig();
196
291
  }
197
292
 
198
293
  public setEnvironment(environment: Environment) {
199
294
  this.loadConfig();
200
295
  this.config.activeEnvironment = environment;
201
- fs.writeFileSync(this.configPath, JSON.stringify(this.config));
296
+ this.writeConfig();
202
297
  }
203
298
 
204
299
  public getEnvironment() {
@@ -245,14 +340,20 @@ export class ClientConfiguration {
245
340
  public printStatus() {
246
341
  this.loadConfig();
247
342
  const tokenStatus = this.isTokenValid();
343
+ const cell = this.getEnvironment();
344
+
345
+ // The cell prints on both branches, and before anything else: an org and an
346
+ // environment name alone do not identify a cell (two cells can both hold a
347
+ // `demo`), and a stale token is most often a token for the wrong one.
348
+ console.log(kleur.green(ASCII_TITLE));
349
+ logger.info(`Cell: ${kleur.green(cell)} (${controlPlanesByEnv[cell]})`);
248
350
 
249
351
  if (tokenStatus.valid) {
250
352
  const organization = this.getOrganization();
251
353
  const project = this.getProject();
252
354
 
253
- console.log(kleur.green(ASCII_TITLE));
254
355
  logger.info(`Organization: ${kleur.green(organization || "Not set")}`);
255
- logger.info(`Project: ${kleur.green(project || "Not set")}`);
356
+ logger.info(`Environment: ${kleur.green(project || "Not set")}`);
256
357
  } else {
257
358
  logger.error("Failed to get status");
258
359
  logger.error(
@@ -1,10 +1,13 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  import { Command } from "commander";
2
3
  import { controlPlanesByEnv, Environment } from "../constants";
3
4
  import { createConnections } from "./connection/createconnections";
4
5
  import { addMember } from "./group/addmember";
5
6
  import { createGroup } from "./group/creategroup";
6
7
  import { createGroupAccessToken } from "./groupaccesstoken/createToken";
7
- import { createProject } from "./project/createproject";
8
+ import { createEnvironment } from "./environment/createenvironment";
9
+ import { warnProjectDeprecated } from "./legacy";
10
+ import { createPermission } from "./permission/createpermission";
8
11
 
9
12
  export const registerAddCommands = (cli: Command, environment: Environment) => {
10
13
  const add = cli
@@ -12,14 +15,41 @@ export const registerAddCommands = (cli: Command, environment: Environment) => {
12
15
  .description("Add a resource")
13
16
  .argument("<resource>", "The resource to add");
14
17
 
18
+ const addEnvironmentAction = (environmentName: string, options: any) => {
19
+ createEnvironment(
20
+ controlPlanesByEnv[environment],
21
+ environmentName,
22
+ options.readmeFile,
23
+ options.replication,
24
+ options.debug,
25
+ options.yes,
26
+ );
27
+ };
28
+
29
+ add.command("environment")
30
+ .description("Add an environment.")
31
+ .argument("<environmentName>", "The name of the environment to create.")
32
+ .option("--debug")
33
+ .option("-y, --yes", "Skip confirmation.")
34
+ .option(
35
+ "--readmeFile <readmeFile>",
36
+ "The path to the readme file for the environment.",
37
+ )
38
+ .option(
39
+ "--replication <replicationCount>",
40
+ "The replication count (must be at least 1).",
41
+ parseInt,
42
+ )
43
+ .action(addEnvironmentAction);
44
+
15
45
  add.command("project")
16
- .description("Add a project.")
17
- .argument("<projectName>", "The name of the project to create.")
46
+ .description("[deprecated] Alias for 'add environment'.")
47
+ .argument("<projectName>", "The name of the environment to create.")
18
48
  .option("--debug")
19
49
  .option("-y, --yes", "Skip confirmation.")
20
50
  .option(
21
51
  "--readmeFile <readmeFile>",
22
- "The path to the readme file for the project.",
52
+ "The path to the readme file for the environment.",
23
53
  )
24
54
  .option(
25
55
  "--replication <replicationCount>",
@@ -27,14 +57,8 @@ export const registerAddCommands = (cli: Command, environment: Environment) => {
27
57
  parseInt,
28
58
  )
29
59
  .action((projectName, options) => {
30
- createProject(
31
- controlPlanesByEnv[environment],
32
- projectName,
33
- options.readmeFile,
34
- options.replication,
35
- options.debug,
36
- options.yes,
37
- );
60
+ warnProjectDeprecated("add environment");
61
+ addEnvironmentAction(projectName, options);
38
62
  });
39
63
 
40
64
  add.command("connection")
@@ -102,6 +126,48 @@ export const registerAddCommands = (cli: Command, environment: Environment) => {
102
126
  );
103
127
  });
104
128
 
129
+ add.command("permission")
130
+ .description(
131
+ "Grant a user or group a permission on the organization, an environment, or a package.",
132
+ )
133
+ .argument(
134
+ "<userGroupId>",
135
+ "Who to grant it to [user:{email}|group:{groupName}|user:*]",
136
+ )
137
+ .argument(
138
+ "<role>",
139
+ "The role to grant (organization: admin|modeler|member; environment: admin|modeler|viewer; package: viewer)",
140
+ )
141
+ .option(
142
+ "--environment <environmentName>",
143
+ "Grant on this environment instead of the organization.",
144
+ )
145
+ .option(
146
+ "--package <packageName>",
147
+ "Grant on this package. Uses --environment, or the current environment.",
148
+ )
149
+ .option(
150
+ "--notifyPeople",
151
+ "Email the person about the new access. Applies to an environment or a package; an organization grant always notifies a user subject.",
152
+ false,
153
+ )
154
+ .option("--debug")
155
+ .option("-y, --yes", "Skip confirmation.")
156
+ .action((userGroupId, role, options) => {
157
+ createPermission(
158
+ controlPlanesByEnv[environment],
159
+ userGroupId,
160
+ role,
161
+ options.debug || false,
162
+ {
163
+ environment: options.environment,
164
+ package: options.package,
165
+ notifyPeople: options.notifyPeople === true,
166
+ yes: options.yes === true,
167
+ },
168
+ );
169
+ });
170
+
105
171
  add.command("group-access-token")
106
172
  .description("Create an access token for a group.")
107
173
  .argument("<groupName>", "The name of the group")
@@ -270,6 +270,13 @@ describe("create-connections", () => {
270
270
  getConfirmationStub = sandbox.stub();
271
271
  readFileSyncStub = sandbox.stub(fs, "readFileSync");
272
272
 
273
+ // Short-circuit loadConfig — createconnections calls getOrganization
274
+ // before it checks doesConfigExists, so we need to stub it or loadConfig
275
+ // would run against the stubbed readFileSync.
276
+ sandbox
277
+ .stub(ClientConfiguration.prototype, "getOrganization")
278
+ .returns("");
279
+ sandbox.stub(ClientConfiguration.prototype, "getProject").returns("");
273
280
  sandbox
274
281
  .stub(ClientConfiguration.prototype, "doesConfigExists")
275
282
  .returns(false);
@@ -309,6 +316,9 @@ describe("create-connections", () => {
309
316
  sandbox
310
317
  .stub(ClientConfiguration.prototype, "doesConfigExists")
311
318
  .returns(true);
319
+ sandbox
320
+ .stub(ClientConfiguration.prototype, "getOrganization")
321
+ .returns("test-org");
312
322
  sandbox.stub(ClientConfiguration.prototype, "getProject").returns("");
313
323
  sandbox
314
324
  .stub(ConnectionsApi.prototype, "createConnection")
@@ -330,7 +340,7 @@ describe("create-connections", () => {
330
340
  expect(createConnectionStub.called).to.be.false;
331
341
  expect(
332
342
  loggerStub.error.calledWith(
333
- sinon.match("Please, make sure to set a project"),
343
+ sinon.match("Please, make sure to set an environment"),
334
344
  ),
335
345
  ).to.be.true;
336
346
  });
@@ -18,15 +18,15 @@ export const createConnections = async (
18
18
  ) => {
19
19
  const clientConfig = new ClientConfiguration();
20
20
  const organizationName = clientConfig.getOrganization();
21
- const projectName = clientConfig.getProject();
21
+ const environmentName = clientConfig.getProject();
22
22
 
23
23
  if (!clientConfig.doesConfigExists()) {
24
24
  logger.error("Please, make sure that you are logged in");
25
25
  return;
26
26
  }
27
27
 
28
- if (!projectName) {
29
- logger.error("Please, make sure to set a project");
28
+ if (!environmentName) {
29
+ logger.error("Please, make sure to set an environment");
30
30
  return;
31
31
  }
32
32
 
@@ -92,7 +92,7 @@ export const createConnections = async (
92
92
 
93
93
  await connectionsApi.createConnection(
94
94
  organizationName,
95
- projectName,
95
+ environmentName,
96
96
  connection,
97
97
  false,
98
98
  {
@@ -131,7 +131,7 @@ describe("delete-connection", () => {
131
131
  expect(deleteConnectionStub.called).to.be.false;
132
132
  expect(
133
133
  loggerStub.error.calledWith(
134
- sinon.match("Please, make sure to set a project"),
134
+ sinon.match("Please, make sure to set an environment"),
135
135
  ),
136
136
  ).to.be.true;
137
137
  });
@@ -14,15 +14,15 @@ export const deleteConnection = async (
14
14
  ) => {
15
15
  const clientConfig = new ClientConfiguration();
16
16
  const organizationName = clientConfig.getOrganization();
17
- const projectName = clientConfig.getProject();
17
+ const environmentName = clientConfig.getProject();
18
18
 
19
19
  if (!clientConfig.doesConfigExists()) {
20
20
  logger.error("Please, make sure that you are logged in");
21
21
  return;
22
22
  }
23
23
 
24
- if (!projectName) {
25
- logger.error("Please, make sure to set a project");
24
+ if (!environmentName) {
25
+ logger.error("Please, make sure to set an environment");
26
26
  return;
27
27
  }
28
28
 
@@ -41,7 +41,7 @@ export const deleteConnection = async (
41
41
  try {
42
42
  await connectionsApi.deleteConnection(
43
43
  organizationName,
44
- projectName,
44
+ environmentName,
45
45
  connectionName,
46
46
  {
47
47
  baseURL: controlPlane,
@@ -113,7 +113,7 @@ describe("list-connections", () => {
113
113
  expect(listConnectionsStub.called).to.be.false;
114
114
  expect(
115
115
  loggerStub.error.calledWith(
116
- sinon.match("Please, make sure to set a project"),
116
+ sinon.match("Please, make sure to set an environment"),
117
117
  ),
118
118
  ).to.be.true;
119
119
  });
@@ -19,15 +19,15 @@ export const listConnections = async (
19
19
  ) => {
20
20
  const clientConfig = new ClientConfiguration();
21
21
  const organizationName = clientConfig.getOrganization();
22
- const projectName = clientConfig.getProject();
22
+ const environmentName = clientConfig.getProject();
23
23
 
24
24
  if (!clientConfig.doesConfigExists()) {
25
25
  logger.error("Please, make sure that you are logged in");
26
26
  return;
27
27
  }
28
28
 
29
- if (!projectName) {
30
- logger.error("Please, make sure to set a project");
29
+ if (!environmentName) {
30
+ logger.error("Please, make sure to set an environment");
31
31
  return;
32
32
  }
33
33
 
@@ -35,7 +35,7 @@ export const listConnections = async (
35
35
  const { limit, offset } = parsePaginationParams(page, pageSize);
36
36
  const response = await connectionsApi.listConnections(
37
37
  organizationName,
38
- projectName,
38
+ environmentName,
39
39
  limit,
40
40
  offset,
41
41
  {