@strapi/cloud-cli 0.0.0-next.9b0c43700e0ad92b33a4c97f1507f9f3243f2b6a → 0.0.0-next.a237dfc081ee4788ee5abb71a735161aebec0ee5

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.
package/dist/index.js CHANGED
@@ -42,7 +42,6 @@ const ora = require("ora");
42
42
  const cliProgress = require("cli-progress");
43
43
  const pkgUp = require("pkg-up");
44
44
  const yup = require("yup");
45
- const _ = require("lodash");
46
45
  const EventSource = require("eventsource");
47
46
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
48
47
  function _interopNamespace(e) {
@@ -80,7 +79,6 @@ const ora__default = /* @__PURE__ */ _interopDefault(ora);
80
79
  const cliProgress__namespace = /* @__PURE__ */ _interopNamespace(cliProgress);
81
80
  const pkgUp__default = /* @__PURE__ */ _interopDefault(pkgUp);
82
81
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
83
- const ___default = /* @__PURE__ */ _interopDefault(_);
84
82
  const EventSource__default = /* @__PURE__ */ _interopDefault(EventSource);
85
83
  const apiConfig = {
86
84
  apiBaseUrl: utils.env("STRAPI_CLI_CLOUD_API", "https://cloud-cli-api.strapi.io"),
@@ -194,7 +192,7 @@ async function saveLocalConfig(data) {
194
192
  await fse__namespace.default.writeJson(configFilePath, data, { encoding: "utf8", spaces: 2, mode: 384 });
195
193
  }
196
194
  const name = "@strapi/cloud-cli";
197
- const version = "4.25.2";
195
+ const version = "4.25.8";
198
196
  const description = "Commands to interact with the Strapi Cloud";
199
197
  const keywords = [
200
198
  "strapi",
@@ -239,7 +237,7 @@ const scripts = {
239
237
  watch: "pack-up watch"
240
238
  };
241
239
  const dependencies = {
242
- "@strapi/utils": "4.25.2",
240
+ "@strapi/utils": "4.25.8",
243
241
  axios: "1.6.0",
244
242
  chalk: "4.1.2",
245
243
  "cli-progress": "3.12.0",
@@ -264,8 +262,8 @@ const devDependencies = {
264
262
  "@types/cli-progress": "3.11.5",
265
263
  "@types/eventsource": "1.1.15",
266
264
  "@types/lodash": "^4.14.191",
267
- "eslint-config-custom": "4.25.2",
268
- tsconfig: "4.25.2"
265
+ "eslint-config-custom": "4.25.8",
266
+ tsconfig: "4.25.8"
269
267
  };
270
268
  const engines = {
271
269
  node: ">=18.0.0 <=20.x.x",
@@ -375,6 +373,20 @@ async function cloudApiFactory({ logger }, token) {
375
373
  throw error;
376
374
  }
377
375
  },
376
+ async listLinkProjects() {
377
+ try {
378
+ const response = await axiosCloudAPI.get("/projects/linkable");
379
+ if (response.status !== 200) {
380
+ throw new Error("Error fetching cloud projects from the server.");
381
+ }
382
+ return response;
383
+ } catch (error) {
384
+ logger.debug(
385
+ "🥲 Oops! Couldn't retrieve your project's list from the server. Please try again."
386
+ );
387
+ throw error;
388
+ }
389
+ },
378
390
  track(event, payload = {}) {
379
391
  return axiosCloudAPI.post("/track", {
380
392
  event,
@@ -684,18 +696,13 @@ async function getProjectNameFromPackageJson(ctx) {
684
696
  return "my-strapi-project";
685
697
  }
686
698
  }
687
- function applyDefaultName(newDefaultName, questions, defaultValues) {
688
- const newDefaultValues = ___default.default.cloneDeep(defaultValues);
689
- newDefaultValues.name = newDefaultName;
690
- const newQuestions = questions.map((question) => {
691
- const questionCopy = ___default.default.cloneDeep(question);
692
- if (questionCopy.name === "name") {
693
- questionCopy.default = newDefaultName;
694
- }
695
- return questionCopy;
696
- });
697
- return { newQuestions, newDefaultValues };
698
- }
699
+ const trackEvent = async (ctx, cloudApiService, eventName, eventData) => {
700
+ try {
701
+ await cloudApiService.track(eventName, eventData);
702
+ } catch (e) {
703
+ ctx.logger.debug(`Failed to track ${eventName}`, e);
704
+ }
705
+ };
699
706
  const openModule$1 = import("open");
700
707
  async function promptLogin(ctx) {
701
708
  const response = await inquirer__default.default.prompt([
@@ -716,13 +723,6 @@ async function loginAction(ctx) {
716
723
  const tokenService = await tokenServiceFactory(ctx);
717
724
  const existingToken = await tokenService.retrieveToken();
718
725
  const cloudApiService = await cloudApiFactory(ctx, existingToken || void 0);
719
- const trackFailedLogin = async () => {
720
- try {
721
- await cloudApiService.track("didNotLogin", { loginMethod: "cli" });
722
- } catch (e) {
723
- logger.debug("Failed to track failed login", e);
724
- }
725
- };
726
726
  if (existingToken) {
727
727
  const isTokenValid = await tokenService.isTokenValid(existingToken);
728
728
  if (isTokenValid) {
@@ -754,11 +754,7 @@ async function loginAction(ctx) {
754
754
  logger.debug(e);
755
755
  return false;
756
756
  }
757
- try {
758
- await cloudApiService.track("willLoginAttempt", {});
759
- } catch (e) {
760
- logger.debug("Failed to track login attempt", e);
761
- }
757
+ await trackEvent(ctx, cloudApiService, "willLoginAttempt", {});
762
758
  logger.debug("🔐 Creating device authentication request...", {
763
759
  client_id: cliConfig2.clientId,
764
760
  scope: cliConfig2.scope,
@@ -838,13 +834,13 @@ async function loginAction(ctx) {
838
834
  "There seems to be a problem with your login information. Please try logging in again."
839
835
  );
840
836
  spinnerFail();
841
- await trackFailedLogin();
837
+ await trackEvent(ctx, cloudApiService, "didNotLogin", { loginMethod: "cli" });
842
838
  return false;
843
839
  }
844
840
  if (e.response?.data.error && !["authorization_pending", "slow_down"].includes(e.response.data.error)) {
845
841
  logger.debug(e);
846
842
  spinnerFail();
847
- await trackFailedLogin();
843
+ await trackEvent(ctx, cloudApiService, "didNotLogin", { loginMethod: "cli" });
848
844
  return false;
849
845
  }
850
846
  await new Promise((resolve) => {
@@ -858,15 +854,50 @@ async function loginAction(ctx) {
858
854
  "To access your dashboard, please copy and paste the following URL into your web browser:"
859
855
  );
860
856
  logger.log(chalk__default.default.underline(`${apiConfig.dashboardBaseUrl}/projects`));
861
- try {
862
- await cloudApiService.track("didLogin", { loginMethod: "cli" });
863
- } catch (e) {
864
- logger.debug("Failed to track login", e);
865
- }
857
+ await trackEvent(ctx, cloudApiService, "didLogin", { loginMethod: "cli" });
866
858
  };
867
859
  await authenticate();
868
860
  return isAuthenticated;
869
861
  }
862
+ function questionDefaultValuesMapper(questionsMap) {
863
+ return (questions) => {
864
+ return questions.map((question) => {
865
+ const questionName = question.name;
866
+ if (questionName in questionsMap) {
867
+ const questionDefault = questionsMap[questionName];
868
+ if (typeof questionDefault === "function") {
869
+ return {
870
+ ...question,
871
+ default: questionDefault(question)
872
+ };
873
+ }
874
+ return {
875
+ ...question,
876
+ default: questionDefault
877
+ };
878
+ }
879
+ return question;
880
+ });
881
+ };
882
+ }
883
+ function getDefaultsFromQuestions(questions) {
884
+ return questions.reduce((acc, question) => {
885
+ if (question.default && question.name) {
886
+ return { ...acc, [question.name]: question.default };
887
+ }
888
+ return acc;
889
+ }, {});
890
+ }
891
+ function getProjectNodeVersionDefault(question) {
892
+ const currentNodeVersion = process.versions.node.split(".")[0];
893
+ if (question.type === "list" && Array.isArray(question.choices)) {
894
+ const choice = question.choices.find((choice2) => choice2.value === currentNodeVersion);
895
+ if (choice) {
896
+ return choice.value;
897
+ }
898
+ }
899
+ return question.default;
900
+ }
870
901
  async function handleError(ctx, error) {
871
902
  const { logger } = ctx;
872
903
  logger.debug(error);
@@ -911,7 +942,7 @@ async function createProject$1(ctx, cloudApi, projectInput) {
911
942
  throw e;
912
943
  }
913
944
  }
914
- const action$3 = async (ctx) => {
945
+ const action$4 = async (ctx) => {
915
946
  const { logger } = ctx;
916
947
  const { getValidToken, eraseToken } = await tokenServiceFactory(ctx);
917
948
  const token = await getValidToken(ctx, promptLogin);
@@ -920,11 +951,16 @@ const action$3 = async (ctx) => {
920
951
  }
921
952
  const cloudApi = await cloudApiFactory(ctx, token);
922
953
  const { data: config } = await cloudApi.config();
923
- const { newQuestions: questions, newDefaultValues: defaultValues } = applyDefaultName(
924
- await getProjectNameFromPackageJson(ctx),
925
- config.projectCreation.questions,
926
- config.projectCreation.defaults
927
- );
954
+ const projectName = await getProjectNameFromPackageJson(ctx);
955
+ const defaultAnswersMapper = questionDefaultValuesMapper({
956
+ name: projectName,
957
+ nodeVersion: getProjectNodeVersionDefault
958
+ });
959
+ const questions = defaultAnswersMapper(config.projectCreation.questions);
960
+ const defaultValues = {
961
+ ...config.projectCreation.defaults,
962
+ ...getDefaultsFromQuestions(questions)
963
+ };
928
964
  const projectAnswersDefaulted = fp.defaults(defaultValues);
929
965
  const projectAnswers = await inquirer__default.default.prompt(questions);
930
966
  const projectInput = projectAnswersDefaulted(projectAnswers);
@@ -1026,6 +1062,7 @@ const buildLogsServiceFactory = ({ logger }) => {
1026
1062
  if (retries > MAX_RETRIES) {
1027
1063
  spinner.fail("We were unable to connect to the server to get build logs at this time.");
1028
1064
  es.close();
1065
+ clearExistingTimeout();
1029
1066
  reject(new Error("Max retries reached"));
1030
1067
  }
1031
1068
  };
@@ -1102,8 +1139,8 @@ async function upload(ctx, project, token, maxProjectFileSize) {
1102
1139
  progressBar.stop();
1103
1140
  if (e instanceof axios.AxiosError && e.response?.data) {
1104
1141
  if (e.response.status === 404) {
1105
- ctx.logger.error(
1106
- `The project does not exist. Remove the ${LOCAL_SAVE_FILENAME} file and try again.`
1142
+ ctx.logger.warn(
1143
+ `The project does not exist. Please link your local project to a Strapi Cloud project using the link command.`
1107
1144
  );
1108
1145
  } else {
1109
1146
  ctx.logger.error(e.response.data);
@@ -1126,7 +1163,7 @@ async function getProject(ctx) {
1126
1163
  const { project } = await retrieve();
1127
1164
  if (!project) {
1128
1165
  try {
1129
- return await action$3(ctx);
1166
+ return await action$4(ctx);
1130
1167
  } catch (e) {
1131
1168
  ctx.logger.error("An error occurred while deploying the project. Please try again later.");
1132
1169
  ctx.logger.debug(e);
@@ -1135,7 +1172,19 @@ async function getProject(ctx) {
1135
1172
  }
1136
1173
  return project;
1137
1174
  }
1138
- const action$2 = async (ctx) => {
1175
+ async function getConfig({
1176
+ ctx,
1177
+ cloudApiService
1178
+ }) {
1179
+ try {
1180
+ const { data: cliConfig2 } = await cloudApiService.config();
1181
+ return cliConfig2;
1182
+ } catch (e) {
1183
+ ctx.logger.debug("Failed to get cli config", e);
1184
+ return null;
1185
+ }
1186
+ }
1187
+ const action$3 = async (ctx) => {
1139
1188
  const { getValidToken } = await tokenServiceFactory(ctx);
1140
1189
  const token = await getValidToken(ctx, promptLogin);
1141
1190
  if (!token) {
@@ -1146,14 +1195,18 @@ const action$2 = async (ctx) => {
1146
1195
  return;
1147
1196
  }
1148
1197
  const cloudApiService = await cloudApiFactory(ctx);
1149
- try {
1150
- await cloudApiService.track("willDeployWithCLI", { projectInternalName: project.name });
1151
- } catch (e) {
1152
- ctx.logger.debug("Failed to track willDeploy", e);
1153
- }
1198
+ await trackEvent(ctx, cloudApiService, "willDeployWithCLI", {
1199
+ projectInternalName: project.name
1200
+ });
1154
1201
  const notificationService = notificationServiceFactory(ctx);
1155
1202
  const buildLogsService = buildLogsServiceFactory(ctx);
1156
- const { data: cliConfig2 } = await cloudApiService.config();
1203
+ const cliConfig2 = await getConfig({ ctx, cloudApiService });
1204
+ if (!cliConfig2) {
1205
+ ctx.logger.error(
1206
+ "An error occurred while retrieving data from Strapi Cloud. Please try check your network or again later."
1207
+ );
1208
+ return;
1209
+ }
1157
1210
  let maxSize = parseInt(cliConfig2.maxProjectFileSize, 10);
1158
1211
  if (Number.isNaN(maxSize)) {
1159
1212
  ctx.logger.debug(
@@ -1175,10 +1228,11 @@ const action$2 = async (ctx) => {
1175
1228
  chalk__default.default.underline(`${apiConfig.dashboardBaseUrl}/projects/${project.name}/deployments`)
1176
1229
  );
1177
1230
  } catch (e) {
1231
+ ctx.logger.debug(e);
1178
1232
  if (e instanceof Error) {
1179
1233
  ctx.logger.error(e.message);
1180
1234
  } else {
1181
- throw e;
1235
+ ctx.logger.error("An error occurred while deploying the project. Please try again later.");
1182
1236
  }
1183
1237
  }
1184
1238
  };
@@ -1209,12 +1263,158 @@ const runAction = (name2, action2) => (...args) => {
1209
1263
  process.exit(1);
1210
1264
  });
1211
1265
  };
1212
- const command$4 = ({ command: command2, ctx }) => {
1213
- command2.command("cloud:deploy").alias("deploy").description("Deploy a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("deploy", action$2)(ctx));
1266
+ const command$5 = ({ command: command2, ctx }) => {
1267
+ command2.command("cloud:deploy").alias("deploy").description("Deploy a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("deploy", action$3)(ctx));
1214
1268
  };
1215
1269
  const deployProject = {
1216
1270
  name: "deploy-project",
1217
1271
  description: "Deploy a Strapi Cloud project",
1272
+ action: action$3,
1273
+ command: command$5
1274
+ };
1275
+ const QUIT_OPTION = "Quit";
1276
+ async function getExistingConfig(ctx) {
1277
+ try {
1278
+ return await retrieve();
1279
+ } catch (e) {
1280
+ ctx.logger.debug("Failed to get project config", e);
1281
+ ctx.logger.error("An error occurred while retrieving config data from your local project.");
1282
+ return null;
1283
+ }
1284
+ }
1285
+ async function promptForRelink(ctx, cloudApiService, existingConfig) {
1286
+ if (existingConfig && existingConfig.project) {
1287
+ const { shouldRelink } = await inquirer__default.default.prompt([
1288
+ {
1289
+ type: "confirm",
1290
+ name: "shouldRelink",
1291
+ message: `A project named ${chalk__default.default.cyan(
1292
+ existingConfig.project.displayName ? existingConfig.project.displayName : existingConfig.project.name
1293
+ )} is already linked to this local folder. Do you want to update the link?`,
1294
+ default: false
1295
+ }
1296
+ ]);
1297
+ if (!shouldRelink) {
1298
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1299
+ currentProjectName: existingConfig.project?.name
1300
+ });
1301
+ return false;
1302
+ }
1303
+ }
1304
+ return true;
1305
+ }
1306
+ async function getProjectsList(ctx, cloudApiService, existingConfig) {
1307
+ const spinner = ctx.logger.spinner("Fetching your projects...\n").start();
1308
+ try {
1309
+ const {
1310
+ data: { data: projectList }
1311
+ } = await cloudApiService.listLinkProjects();
1312
+ spinner.succeed();
1313
+ if (!Array.isArray(projectList)) {
1314
+ ctx.logger.log("We couldn't find any projects available for linking in Strapi Cloud");
1315
+ return null;
1316
+ }
1317
+ const projects = projectList.filter(
1318
+ (project) => !(project.isMaintainer || project.name === existingConfig?.project?.name)
1319
+ ).map((project) => {
1320
+ return {
1321
+ name: project.displayName,
1322
+ value: { name: project.name, displayName: project.displayName }
1323
+ };
1324
+ });
1325
+ if (projects.length === 0) {
1326
+ ctx.logger.log("We couldn't find any projects available for linking in Strapi Cloud");
1327
+ return null;
1328
+ }
1329
+ return projects;
1330
+ } catch (e) {
1331
+ spinner.fail("An error occurred while fetching your projects from Strapi Cloud.");
1332
+ ctx.logger.debug("Failed to list projects", e);
1333
+ return null;
1334
+ }
1335
+ }
1336
+ async function getUserSelection(ctx, projects) {
1337
+ const { logger } = ctx;
1338
+ try {
1339
+ const answer = await inquirer__default.default.prompt([
1340
+ {
1341
+ type: "list",
1342
+ name: "linkProject",
1343
+ message: "Which project do you want to link?",
1344
+ choices: [...projects, { name: chalk__default.default.grey(`(${QUIT_OPTION})`), value: null }]
1345
+ }
1346
+ ]);
1347
+ if (!answer.linkProject) {
1348
+ return null;
1349
+ }
1350
+ return answer;
1351
+ } catch (e) {
1352
+ logger.debug("Failed to get user input", e);
1353
+ logger.error("An error occurred while trying to get your input.");
1354
+ return null;
1355
+ }
1356
+ }
1357
+ const action$2 = async (ctx) => {
1358
+ const { getValidToken } = await tokenServiceFactory(ctx);
1359
+ const token = await getValidToken(ctx, promptLogin);
1360
+ const { logger } = ctx;
1361
+ if (!token) {
1362
+ return;
1363
+ }
1364
+ const cloudApiService = await cloudApiFactory(ctx, token);
1365
+ const existingConfig = await getExistingConfig(ctx);
1366
+ const shouldRelink = await promptForRelink(ctx, cloudApiService, existingConfig);
1367
+ if (!shouldRelink) {
1368
+ return;
1369
+ }
1370
+ await trackEvent(ctx, cloudApiService, "willLinkProject", {});
1371
+ const projects = await getProjectsList(
1372
+ ctx,
1373
+ cloudApiService,
1374
+ existingConfig
1375
+ );
1376
+ if (!projects) {
1377
+ return;
1378
+ }
1379
+ const answer = await getUserSelection(ctx, projects);
1380
+ if (!answer) {
1381
+ return;
1382
+ }
1383
+ try {
1384
+ const { confirmAction } = await inquirer__default.default.prompt([
1385
+ {
1386
+ type: "confirm",
1387
+ name: "confirmAction",
1388
+ message: "Warning: Once linked, deploying from CLI will replace the existing project and its data. Confirm to proceed:",
1389
+ default: false
1390
+ }
1391
+ ]);
1392
+ if (!confirmAction) {
1393
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1394
+ cancelledProjectName: answer.linkProject.name,
1395
+ currentProjectName: existingConfig ? existingConfig.project?.name : null
1396
+ });
1397
+ return;
1398
+ }
1399
+ await save({ project: answer.linkProject });
1400
+ logger.log(`Project ${chalk__default.default.cyan(answer.linkProject.displayName)} linked successfully.`);
1401
+ await trackEvent(ctx, cloudApiService, "didLinkProject", {
1402
+ projectInternalName: answer.linkProject
1403
+ });
1404
+ } catch (e) {
1405
+ logger.debug("Failed to link project", e);
1406
+ logger.error("An error occurred while linking the project.");
1407
+ await trackEvent(ctx, cloudApiService, "didNotLinkProject", {
1408
+ projectInternalName: answer.linkProject
1409
+ });
1410
+ }
1411
+ };
1412
+ const command$4 = ({ command: command2, ctx }) => {
1413
+ command2.command("cloud:link").alias("link").description("Link a local directory to a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("link", action$2)(ctx));
1414
+ };
1415
+ const link = {
1416
+ name: "link-project",
1417
+ description: "Link a local directory to a Strapi Cloud project",
1218
1418
  action: action$2,
1219
1419
  command: command$4
1220
1420
  };
@@ -1261,11 +1461,7 @@ const action$1 = async (ctx) => {
1261
1461
  logger.error("🥲 Oops! Something went wrong while logging you out. Please try again.");
1262
1462
  logger.debug(e);
1263
1463
  }
1264
- try {
1265
- await cloudApiService.track("didLogout", { loginMethod: "cli" });
1266
- } catch (e) {
1267
- logger.debug("Failed to track logout event", e);
1268
- }
1464
+ await trackEvent(ctx, cloudApiService, "didLogout", { loginMethod: "cli" });
1269
1465
  };
1270
1466
  const command$2 = ({ command: command2, ctx }) => {
1271
1467
  command2.command("cloud:logout").alias("logout").description("Strapi Cloud Logout").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("logout", action$1)(ctx));
@@ -1277,12 +1473,12 @@ const logout = {
1277
1473
  command: command$2
1278
1474
  };
1279
1475
  const command$1 = ({ command: command2, ctx }) => {
1280
- command2.command("cloud:create-project").description("Create a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("cloud:create-project", action$3)(ctx));
1476
+ command2.command("cloud:create-project").description("Create a Strapi Cloud project").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("cloud:create-project", action$4)(ctx));
1281
1477
  };
1282
1478
  const createProject = {
1283
1479
  name: "create-project",
1284
1480
  description: "Create a new project",
1285
- action: action$3,
1481
+ action: action$4,
1286
1482
  command: command$1
1287
1483
  };
1288
1484
  const action = async (ctx) => {
@@ -1306,7 +1502,7 @@ const action = async (ctx) => {
1306
1502
  }
1307
1503
  };
1308
1504
  const command = ({ command: command2, ctx }) => {
1309
- command2.command("cloud:projects").alias("projects").description("List Strapi Cloud projects").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("listProjects", action)(ctx));
1505
+ command2.command("cloud:projects").alias("projects").description("List Strapi Cloud projects").option("-d, --debug", "Enable debugging mode with verbose logs").option("-s, --silent", "Don't log anything").action(() => runAction("projects", action)(ctx));
1310
1506
  };
1311
1507
  const listProjects = {
1312
1508
  name: "list-projects",
@@ -1316,12 +1512,13 @@ const listProjects = {
1316
1512
  };
1317
1513
  const cli = {
1318
1514
  deployProject,
1515
+ link,
1319
1516
  login,
1320
1517
  logout,
1321
1518
  createProject,
1322
1519
  listProjects
1323
1520
  };
1324
- const cloudCommands = [deployProject, login, logout, listProjects];
1521
+ const cloudCommands = [deployProject, link, login, logout, listProjects];
1325
1522
  async function initCloudCLIConfig() {
1326
1523
  const localConfig = await getLocalConfig();
1327
1524
  if (!localConfig.deviceId) {