qualty 0.1.25 → 0.1.27

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 (2) hide show
  1. package/bin/qualty.js +325 -36
  2. package/package.json +1 -1
package/bin/qualty.js CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  const DEFAULT_QUALTY_API_URL = "https://qualty-api-production.up.railway.app";
31
31
 
32
32
  const PROJECT_CONFIG_FILENAME = ".qualty.json";
33
+ const DOTENV_FILENAME = ".env";
33
34
  const QUALTY_SHARED_CREDS_DIR = join(homedir(), ".qualty");
34
35
  const QUALTY_SHARED_CREDS_PATH = join(QUALTY_SHARED_CREDS_DIR, "credentials");
35
36
 
@@ -48,6 +49,47 @@ function getProjectConfigWritePath() {
48
49
  return findProjectConfigPath() || join(process.cwd(), PROJECT_CONFIG_FILENAME);
49
50
  }
50
51
 
52
+ function findProjectDotenvPath() {
53
+ let current = process.cwd();
54
+ for (;;) {
55
+ const candidate = join(current, DOTENV_FILENAME);
56
+ if (existsSync(candidate)) return candidate;
57
+ const parent = dirname(current);
58
+ if (parent === current) return null;
59
+ current = parent;
60
+ }
61
+ }
62
+
63
+ function parseDotenvText(text) {
64
+ const out = {};
65
+ for (const raw of String(text || "").split("\n")) {
66
+ const line = raw.trim();
67
+ if (!line || line.startsWith("#") || !line.includes("=")) continue;
68
+ const eq = line.indexOf("=");
69
+ const key = line.slice(0, eq).trim();
70
+ let value = line.slice(eq + 1).trim();
71
+ if (
72
+ (value.startsWith('"') && value.endsWith('"'))
73
+ || (value.startsWith("'") && value.endsWith("'"))
74
+ ) {
75
+ value = value.slice(1, -1);
76
+ }
77
+ if (key) out[key] = value;
78
+ }
79
+ return out;
80
+ }
81
+
82
+ /** Project .env walked up from cwd (same resolution as Qualty MCP). */
83
+ function loadProjectDotenv() {
84
+ const path = findProjectDotenvPath();
85
+ if (!path) return {};
86
+ try {
87
+ return parseDotenvText(readFileSync(path, "utf8"));
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
92
+
51
93
  function readSharedCredentials() {
52
94
  if (!existsSync(QUALTY_SHARED_CREDS_PATH)) return {};
53
95
  try {
@@ -124,7 +166,14 @@ function normalizeApiUrl(raw, { label = "API URL" } = {}) {
124
166
  }
125
167
 
126
168
  function qualtyBaseUrlFromEnv() {
127
- return String(process.env.QUALTY_BASE_URL || process.env.QUALTY_API_URL || "").trim();
169
+ const dotenv = loadProjectDotenv();
170
+ return String(
171
+ dotenv.QUALTY_BASE_URL
172
+ || dotenv.QUALTY_API_URL
173
+ || process.env.QUALTY_BASE_URL
174
+ || process.env.QUALTY_API_URL
175
+ || ""
176
+ ).trim();
128
177
  }
129
178
 
130
179
  function resolveApiUrl(args = {}) {
@@ -142,8 +191,14 @@ function resolveApiUrl(args = {}) {
142
191
  }
143
192
 
144
193
  function resolveToken(args = {}) {
194
+ if (args.token) return String(args.token).trim();
195
+ const dotenv = loadProjectDotenv();
196
+ const fromDotenv = String(dotenv.QUALTY_API_TOKEN || "").trim();
197
+ if (fromDotenv) return fromDotenv;
198
+ const fromProcessEnv = String(process.env.QUALTY_API_TOKEN || "").trim();
199
+ if (fromProcessEnv) return fromProcessEnv;
145
200
  const creds = readSharedCredentials();
146
- return args.token || process.env.QUALTY_API_TOKEN || creds.api_token || "";
201
+ return String(creds.api_token || "").trim();
147
202
  }
148
203
 
149
204
  function resolveOrgId(args = {}) {
@@ -187,13 +242,17 @@ function usage() {
187
242
  "Usage:",
188
243
  " qualty setup [--token <bearer-token>] [--org <org-id>]",
189
244
  " qualty auth setup [--project <project-id>] [--profile <profile-name>] [--headed]",
190
- " qualty auth export-ci [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
191
- " [--all-profiles] [--profiles <name1,name2>]",
192
- " [--suite-id <suite-uuid>] [--ids <id1,id2>] export profiles required by tests",
193
- " [--url <api-base-url>] [--repo <owner/repo>]",
194
- " [--output <path>] [--no-file] [--copy] [--gzip] [--include-token]",
195
- " [--set-secret | --set-github] gh: secrets (token, API URL, auth bundle) + variables",
196
- " [--cli-repo <owner/repo>] [--cli-ref <branch>] Optional CI variables",
245
+ " qualty auth export [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
246
+ " [--profiles <name1,name2>] [--repo <owner/repo>]",
247
+ " [--output <path>] [--no-file] [--copy] [--set-secret | --set-github]",
248
+ " Push login profile state to GitHub only (not API token, org, or project vars).",
249
+ " qualty ci setup [--project <project-id>] [--profile <profile-name>] [--org <org-id>]",
250
+ " [--all-profiles] [--profiles <name1,name2>]",
251
+ " [--suite-id <suite-uuid>] [--ids <id1,id2>] bundle login profiles required by tests",
252
+ " [--url <api-base-url>] [--repo <owner/repo>]",
253
+ " [--output <path>] [--no-file] [--copy] [--gzip] [--include-token]",
254
+ " [--set-secret | --set-github] gh: secrets (token, API URL, auth bundle) + variables",
255
+ " [--cli-repo <owner/repo>] [--cli-ref <branch>] Optional CI variables",
197
256
  " qualty auth restore-ci [--org <org-id>] [--project <project-id>] [--profile <fallback-profile>]",
198
257
  " Reads QUALTY_AUTH_STATE_B64 (+ _2, _3, … shards) and writes ~/.qualty/local-contexts/…",
199
258
  " qualty connect --project <project-id> [--port 3000] [--token <bearer-token>] [--org <org-id>]",
@@ -206,7 +265,7 @@ function usage() {
206
265
  " qualty resolve --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--json] [--token <bearer-token>]",
207
266
  "",
208
267
  "Env vars:",
209
- " QUALTY_API_TOKEN Bearer token used for auth",
268
+ " QUALTY_API_TOKEN Project .env first (same as MCP), then process env, then ~/.qualty/credentials",
210
269
  " QUALTY_BASE_URL API base URL when --url is not set (default: production)",
211
270
  " QUALTY_LOCAL_CONCURRENCY Local parallel workers for --local runs (default: 4)",
212
271
  "",
@@ -393,9 +452,7 @@ async function runSetup(args) {
393
452
 
394
453
  try {
395
454
  const currentOrgId = String(args.org || projectCfg.default_org_id || "");
396
- const currentToken = String(
397
- args.token || process.env.QUALTY_API_TOKEN || sharedCreds.api_token || ""
398
- );
455
+ const currentToken = resolveToken(args);
399
456
 
400
457
  let token = "";
401
458
  if (args.token) {
@@ -542,7 +599,7 @@ function localAuthStatePath({ orgId, projectId, profile }) {
542
599
  return join(QUALTY_SHARED_CREDS_DIR, "local-contexts", orgSeg, projectSeg, `${profileSeg}.json`);
543
600
  }
544
601
 
545
- function authExportCiOutputPath(statePath) {
602
+ function ciSetupAuthBundleOutputPath(statePath) {
546
603
  return statePath.replace(/\.json$/i, ".b64");
547
604
  }
548
605
 
@@ -674,6 +731,37 @@ function syncGithubCiConfig({
674
731
  return results;
675
732
  }
676
733
 
734
+ /**
735
+ * Push only auth bundle secrets and auth-related repository variables (no API token or org/project).
736
+ */
737
+ function syncGithubAuthProfileOnly({ repo, profile, profileNames, authShards }) {
738
+ const results = [];
739
+ const record = (kind, name, outcome) => {
740
+ results.push({ kind, name, ...outcome });
741
+ };
742
+
743
+ for (const shard of authShards || []) {
744
+ record("secret", shard.secretName, setGithubSecret(shard.secretName, shard.payload, { repo }));
745
+ }
746
+
747
+ const names = (profileNames || []).filter(Boolean);
748
+ if (names.length > 0) {
749
+ record("variable", AUTH_PROFILES_VARIABLE, setGithubVariable(AUTH_PROFILES_VARIABLE, names.join(","), { repo }));
750
+ }
751
+ if (profile) {
752
+ record("variable", "QUALTY_AUTH_PROFILE", setGithubVariable("QUALTY_AUTH_PROFILE", profile, { repo }));
753
+ }
754
+ if ((authShards || []).length > 1) {
755
+ record(
756
+ "variable",
757
+ AUTH_SHARDS_VARIABLE,
758
+ setGithubVariable(AUTH_SHARDS_VARIABLE, String(authShards.length), { repo })
759
+ );
760
+ }
761
+
762
+ return results;
763
+ }
764
+
677
765
  function printGithubSyncResults(results, { repo }) {
678
766
  const target = repo ? ` (repo ${repo})` : "";
679
767
  const printGroup = (title, rows) => {
@@ -754,7 +842,7 @@ function configStringValue(projectCfg, key, configPath) {
754
842
  );
755
843
  }
756
844
 
757
- function missingExportCiField(fieldLabel, { flag, configKey, configPath, hasFlag }) {
845
+ function missingCiSetupField(fieldLabel, { flag, configKey, configPath, hasFlag }) {
758
846
  const configHint = configPath
759
847
  ? `${PROJECT_CONFIG_FILENAME} at ${configPath} (${configKey})`
760
848
  : `${PROJECT_CONFIG_FILENAME} (not found walking up from ${process.cwd()})`;
@@ -770,7 +858,7 @@ function missingExportCiField(fieldLabel, { flag, configKey, configPath, hasFlag
770
858
  ].join(" ");
771
859
  }
772
860
 
773
- function resolveExportCiConfig(args, { requireProfile = true } = {}) {
861
+ function resolveCiSetupConfig(args, { requireProfile = true } = {}) {
774
862
  const { configPath, config: projectCfg } = readProjectConfigStrict();
775
863
 
776
864
  const orgFromFlag = String(args.org || "").trim();
@@ -804,7 +892,7 @@ function resolveExportCiConfig(args, { requireProfile = true } = {}) {
804
892
 
805
893
  if (!orgId) {
806
894
  throw new Error(
807
- missingExportCiField("org ID", {
895
+ missingCiSetupField("org ID", {
808
896
  flag: "--org",
809
897
  configKey: "default_org_id",
810
898
  configPath,
@@ -814,7 +902,7 @@ function resolveExportCiConfig(args, { requireProfile = true } = {}) {
814
902
  }
815
903
  if (!projectId) {
816
904
  throw new Error(
817
- missingExportCiField("project ID", {
905
+ missingCiSetupField("project ID", {
818
906
  flag: "--project",
819
907
  configKey: "default_project_id",
820
908
  configPath,
@@ -827,7 +915,7 @@ function resolveExportCiConfig(args, { requireProfile = true } = {}) {
827
915
  allProfiles || profilesList.length > 0 || Boolean(suiteId) || explicitIds.length > 0;
828
916
  if (requireProfile && !profile && !hasMultiProfileMode) {
829
917
  throw new Error(
830
- missingExportCiField("auth profile", {
918
+ missingCiSetupField("auth profile", {
831
919
  flag: "--profile",
832
920
  configKey: "default_auth_profile",
833
921
  configPath,
@@ -910,7 +998,7 @@ async function resolveRequiredProfileNamesFromTests({
910
998
  return [...names];
911
999
  }
912
1000
 
913
- async function resolveExportProfileEntries({
1001
+ async function resolveCiSetupProfileEntries({
914
1002
  orgId,
915
1003
  projectId,
916
1004
  profile,
@@ -1033,7 +1121,7 @@ async function runAuthRestoreCi(args) {
1033
1121
  const payloads = collectAuthShardPayloadsFromEnv(process.env);
1034
1122
  if (payloads.length === 0) {
1035
1123
  throw new Error(
1036
- `Missing ${AUTH_SECRET_PRIMARY}. Run qualty auth export-ci --set-github from your app repo.`
1124
+ `Missing ${AUTH_SECRET_PRIMARY}. Run qualty ci setup --set-github (first time) or qualty auth export --set-github (profile refresh).`
1037
1125
  );
1038
1126
  }
1039
1127
 
@@ -1052,7 +1140,198 @@ async function runAuthRestoreCi(args) {
1052
1140
  console.log(`Restored ${restored.length} login profile(s).`);
1053
1141
  }
1054
1142
 
1055
- async function runAuthExportCi(args) {
1143
+ async function runAuthExport(args) {
1144
+ if (
1145
+ parseBoolean(args["all-profiles"], false) ||
1146
+ String(args["suite-id"] || args.suite || "").trim() ||
1147
+ String(args.ids || "")
1148
+ .split(",")
1149
+ .map((part) => part.trim())
1150
+ .filter(Boolean).length > 0
1151
+ ) {
1152
+ throw new Error(
1153
+ "qualty auth export only updates login profile secrets. For full CI bootstrap use: qualty ci setup"
1154
+ );
1155
+ }
1156
+
1157
+ const profilesList = String(args.profiles || "")
1158
+ .split(",")
1159
+ .map((part) => part.trim())
1160
+ .filter(Boolean);
1161
+ const { orgId, projectId, profile, apiUrl, configPath } = resolveCiSetupConfig(args, {
1162
+ requireProfile: profilesList.length === 0,
1163
+ });
1164
+ const token = resolveToken(args);
1165
+
1166
+ const profileEntries = await resolveCiSetupProfileEntries({
1167
+ orgId,
1168
+ projectId,
1169
+ profile,
1170
+ allProfiles: false,
1171
+ profilesList,
1172
+ suiteId: "",
1173
+ explicitIds: [],
1174
+ apiUrl,
1175
+ token,
1176
+ });
1177
+ const profileNames = profileEntries.map((entry) => entry.name);
1178
+ const primaryProfile = profile || profileNames[0] || "";
1179
+
1180
+ const manifest = buildAuthBundleManifest({ orgId, projectId, profileEntries });
1181
+ const authShards = shardAuthBundleManifest(manifest);
1182
+ const primaryShard = authShards[0];
1183
+
1184
+ const setGithubFlag =
1185
+ parseBoolean(args["set-github"], false) || parseBoolean(args["set-secret"], false);
1186
+ const noFile = parseBoolean(args["no-file"], false);
1187
+ const outputPath = noFile
1188
+ ? String(args.output || "").trim()
1189
+ : String(args.output || "").trim() ||
1190
+ ciSetupAuthBundleOutputPath(
1191
+ localAuthStatePath({ orgId, projectId, profile: profileEntries[0].localProfileName })
1192
+ );
1193
+ const copyToClipboardFlag = parseBoolean(args.copy, false);
1194
+ const githubRepo = String(args.repo || "").trim();
1195
+
1196
+ if (outputPath) {
1197
+ writeFileSync(outputPath, primaryShard.payload, "utf8");
1198
+ try {
1199
+ chmodSync(outputPath, 0o600);
1200
+ } catch {
1201
+ // ignore platforms that do not support chmod
1202
+ }
1203
+ for (let index = 1; index < authShards.length; index += 1) {
1204
+ const shard = authShards[index];
1205
+ const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
1206
+ writeFileSync(shardPath, shard.payload, "utf8");
1207
+ try {
1208
+ chmodSync(shardPath, 0o600);
1209
+ } catch {
1210
+ // ignore
1211
+ }
1212
+ }
1213
+ }
1214
+
1215
+ const skipPayloadPrint = Boolean(outputPath || copyToClipboardFlag || setGithubFlag);
1216
+ const totalSecretBytes = authShards.reduce(
1217
+ (sum, shard) => sum + Buffer.byteLength(shard.payload, "utf8"),
1218
+ 0
1219
+ );
1220
+
1221
+ // eslint-disable-next-line no-console
1222
+ console.log("");
1223
+ // eslint-disable-next-line no-console
1224
+ console.log("=== Qualty → GitHub Actions (login profiles only) ===");
1225
+ // eslint-disable-next-line no-console
1226
+ console.log("");
1227
+ // eslint-disable-next-line no-console
1228
+ console.log(
1229
+ `Auth bundle: ${profileNames.length} profile(s), ${authShards.length} secret shard(s), ${totalSecretBytes} bytes total`
1230
+ );
1231
+ for (const shard of authShards) {
1232
+ // eslint-disable-next-line no-console
1233
+ console.log(
1234
+ ` - ${shard.secretName}: ${shard.profileNames.join(", ")} (${Buffer.byteLength(shard.payload, "utf8")} bytes)`
1235
+ );
1236
+ }
1237
+ // eslint-disable-next-line no-console
1238
+ console.log("");
1239
+ if (outputPath) {
1240
+ // eslint-disable-next-line no-console
1241
+ console.log(`Wrote ${AUTH_SECRET_PRIMARY} to ${outputPath}`);
1242
+ for (let index = 1; index < authShards.length; index += 1) {
1243
+ const shard = authShards[index];
1244
+ const shardPath = outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
1245
+ // eslint-disable-next-line no-console
1246
+ console.log(`Wrote ${shard.secretName} to ${shardPath}`);
1247
+ }
1248
+ // eslint-disable-next-line no-console
1249
+ console.log("");
1250
+ }
1251
+ if (setGithubFlag) {
1252
+ const syncResults = syncGithubAuthProfileOnly({
1253
+ repo: githubRepo,
1254
+ profile: primaryProfile,
1255
+ profileNames,
1256
+ authShards,
1257
+ });
1258
+ printGithubSyncResults(syncResults, { repo: githubRepo });
1259
+ const failedAuth = syncResults.filter((row) => row.kind === "secret" && !row.ok);
1260
+ if (failedAuth.length > 0 && outputPath) {
1261
+ // eslint-disable-next-line no-console
1262
+ console.log(" Manual auth secrets:");
1263
+ authShards.forEach((shard, index) => {
1264
+ const shardPath =
1265
+ index === 0 ? outputPath : outputPath.replace(/(\.[^./]+)?$/, `_${index + 1}$1`);
1266
+ // eslint-disable-next-line no-console
1267
+ console.log(` gh secret set ${shard.secretName} < ${shardPath}`);
1268
+ });
1269
+ // eslint-disable-next-line no-console
1270
+ console.log("");
1271
+ }
1272
+ } else {
1273
+ // eslint-disable-next-line no-console
1274
+ console.log("GitHub secrets (login state only — does not update QUALTY_API_TOKEN or org/project):");
1275
+ // eslint-disable-next-line no-console
1276
+ console.log("");
1277
+ for (const shard of authShards) {
1278
+ // eslint-disable-next-line no-console
1279
+ console.log("---");
1280
+ // eslint-disable-next-line no-console
1281
+ console.log(`Name: ${shard.secretName}`);
1282
+ if (skipPayloadPrint) {
1283
+ // eslint-disable-next-line no-console
1284
+ console.log(
1285
+ `Value: (see file above${copyToClipboardFlag && shard.secretName === AUTH_SECRET_PRIMARY ? " or clipboard" : ""})`
1286
+ );
1287
+ } else {
1288
+ // eslint-disable-next-line no-console
1289
+ console.log("Value:");
1290
+ // eslint-disable-next-line no-console
1291
+ console.log(shard.payload);
1292
+ }
1293
+ }
1294
+ // eslint-disable-next-line no-console
1295
+ console.log("---");
1296
+ // eslint-disable-next-line no-console
1297
+ console.log("");
1298
+ // eslint-disable-next-line no-console
1299
+ console.log(
1300
+ "Re-run with --set-github from this repo's git root to push via gh (after qualty ci setup for first-time CI)."
1301
+ );
1302
+ // eslint-disable-next-line no-console
1303
+ console.log("");
1304
+ }
1305
+ if (copyToClipboardFlag) {
1306
+ if (copyToClipboard(primaryShard.payload)) {
1307
+ // eslint-disable-next-line no-console
1308
+ console.log(`✓ Copied ${AUTH_SECRET_PRIMARY} to clipboard.`);
1309
+ } else {
1310
+ // eslint-disable-next-line no-console
1311
+ console.log("Could not copy to clipboard on this machine.");
1312
+ }
1313
+ // eslint-disable-next-line no-console
1314
+ console.log("");
1315
+ }
1316
+ for (const entry of profileEntries) {
1317
+ // eslint-disable-next-line no-console
1318
+ console.log(`Profile "${entry.name}" → ${entry.statePath}`);
1319
+ }
1320
+ if (configPath) {
1321
+ // eslint-disable-next-line no-console
1322
+ console.log(`Config: ${configPath}`);
1323
+ }
1324
+ // eslint-disable-next-line no-console
1325
+ console.log("");
1326
+ // eslint-disable-next-line no-console
1327
+ console.log(
1328
+ "When login expires, re-run qualty auth setup then qualty auth export --set-github (or qualty ci setup for a full refresh)."
1329
+ );
1330
+ // eslint-disable-next-line no-console
1331
+ console.log("");
1332
+ }
1333
+
1334
+ async function runCiSetup(args) {
1056
1335
  const {
1057
1336
  orgId,
1058
1337
  projectId,
@@ -1061,18 +1340,18 @@ async function runAuthExportCi(args) {
1061
1340
  configPath,
1062
1341
  allProfiles,
1063
1342
  profilesList,
1064
- suiteId: exportSuiteId,
1343
+ suiteId: configSuiteId,
1065
1344
  explicitIds,
1066
- } = resolveExportCiConfig(args);
1345
+ } = resolveCiSetupConfig(args);
1067
1346
  const token = resolveToken(args);
1068
1347
 
1069
- const profileEntries = await resolveExportProfileEntries({
1348
+ const profileEntries = await resolveCiSetupProfileEntries({
1070
1349
  orgId,
1071
1350
  projectId,
1072
1351
  profile,
1073
1352
  allProfiles,
1074
1353
  profilesList,
1075
- suiteId: exportSuiteId,
1354
+ suiteId: configSuiteId,
1076
1355
  explicitIds,
1077
1356
  apiUrl,
1078
1357
  token,
@@ -1090,12 +1369,12 @@ async function runAuthExportCi(args) {
1090
1369
  const outputPath = noFile
1091
1370
  ? String(args.output || "").trim()
1092
1371
  : String(args.output || "").trim() ||
1093
- authExportCiOutputPath(
1372
+ ciSetupAuthBundleOutputPath(
1094
1373
  localAuthStatePath({ orgId, projectId, profile: profileEntries[0].localProfileName })
1095
1374
  );
1096
1375
  const copyToClipboardFlag = parseBoolean(args.copy, false);
1097
1376
  const githubRepo = String(args.repo || "").trim();
1098
- const suiteId = String(args["suite-id"] || args.suite || exportSuiteId || "").trim();
1377
+ const suiteId = String(args["suite-id"] || args.suite || configSuiteId || "").trim();
1099
1378
  const cliRepo = String(args["cli-repo"] || "").trim();
1100
1379
  const cliRef = String(args["cli-ref"] || "").trim();
1101
1380
 
@@ -1202,10 +1481,10 @@ async function runAuthExportCi(args) {
1202
1481
  console.log(
1203
1482
  "1. GitHub Actions config was pushed via gh (secrets + repository variables — see sync above). Fix any ✗ manually."
1204
1483
  );
1205
- if (!suiteId && !exportSuiteId && explicitIds.length === 0) {
1484
+ if (!suiteId && !configSuiteId && explicitIds.length === 0) {
1206
1485
  // eslint-disable-next-line no-console
1207
1486
  console.log(
1208
- " Tip: pass --suite-id or --ids to export only login profiles required by those tests."
1487
+ " Tip: pass --suite-id or --ids to bundle only login profiles required by those tests."
1209
1488
  );
1210
1489
  }
1211
1490
  if (authShards.length > 1) {
@@ -1359,7 +1638,9 @@ async function runAuthExportCi(args) {
1359
1638
  // eslint-disable-next-line no-console
1360
1639
  console.log("");
1361
1640
  // eslint-disable-next-line no-console
1362
- console.log("When login expires, re-run qualty auth setup and qualty auth export-ci to refresh secrets.");
1641
+ console.log(
1642
+ "When login expires, re-run qualty auth setup and qualty auth export --set-github (or qualty ci setup --set-github for a full refresh)."
1643
+ );
1363
1644
  // eslint-disable-next-line no-console
1364
1645
  console.log("");
1365
1646
  }
@@ -2472,8 +2753,8 @@ async function runAuthSetup(args) {
2472
2753
  console.log("");
2473
2754
  // eslint-disable-next-line no-console
2474
2755
  console.log(
2475
- `[qualty][auth] For GitHub Actions secrets, run:\n` +
2476
- ` qualty auth export-ci --project ${project.id} --profile "${profile}"`
2756
+ `[qualty][auth] Push this profile to GitHub Actions:\n` +
2757
+ ` qualty auth export --project ${project.id} --profile "${profile}" --set-github`
2477
2758
  );
2478
2759
  } finally {
2479
2760
  rl.close();
@@ -2495,21 +2776,29 @@ async function main() {
2495
2776
  await runSetup(args);
2496
2777
  return;
2497
2778
  }
2779
+ if (command === "ci") {
2780
+ const sub = args._[1];
2781
+ if (sub === "setup") {
2782
+ await runCiSetup(args);
2783
+ return;
2784
+ }
2785
+ throw new Error("Unknown ci subcommand. Use: qualty ci setup");
2786
+ }
2498
2787
  if (command === "auth") {
2499
2788
  const sub = args._[1];
2500
2789
  if (sub === "setup") {
2501
2790
  await runAuthSetup(args);
2502
2791
  return;
2503
2792
  }
2504
- if (sub === "export-ci") {
2505
- await runAuthExportCi(args);
2793
+ if (sub === "export") {
2794
+ await runAuthExport(args);
2506
2795
  return;
2507
2796
  }
2508
2797
  if (sub === "restore-ci") {
2509
2798
  await runAuthRestoreCi(args);
2510
2799
  return;
2511
2800
  }
2512
- throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth export-ci | qualty auth restore-ci");
2801
+ throw new Error("Unknown auth subcommand. Use: qualty auth setup | qualty auth export | qualty auth restore-ci");
2513
2802
  }
2514
2803
  if (command === "run") {
2515
2804
  if (parseBoolean(args.local, false)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualty",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Qualty CLI for localhost and CI test runs",
5
5
  "bin": {
6
6
  "qualty": "bin/qualty.js"