@quonfig/cli 0.0.12 → 0.0.14

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 (75) hide show
  1. package/README.md +91 -33
  2. package/dist/codegen/json-schema-to-zod.js +7 -5
  3. package/dist/codegen/language-mappers/zod-base-mapper.d.ts +2 -2
  4. package/dist/codegen/language-mappers/zod-to-string-mapper.d.ts +2 -2
  5. package/dist/codegen/language-mappers/zod-to-string-mapper.js +6 -6
  6. package/dist/codegen/language-mappers/zod-to-typescript-mapper.d.ts +2 -2
  7. package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +6 -6
  8. package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.d.ts +2 -2
  9. package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.js +5 -5
  10. package/dist/codegen/local-config-reader.js +0 -1
  11. package/dist/codegen/schema-extractor.js +5 -2
  12. package/dist/commands/info.js +4 -1
  13. package/dist/commands/init.js +12 -2
  14. package/dist/commands/interactive.d.ts +1 -0
  15. package/dist/commands/interactive.js +8 -0
  16. package/dist/commands/login.js +9 -3
  17. package/dist/commands/migrate/doctor.js +1 -1
  18. package/dist/commands/migrate/my-code.d.ts +11 -0
  19. package/dist/commands/migrate/my-code.js +80 -0
  20. package/dist/commands/migrate/status.js +3 -16
  21. package/dist/commands/migrate.js +29 -8
  22. package/dist/commands/override.js +6 -3
  23. package/dist/commands/profile.d.ts +1 -1
  24. package/dist/commands/profile.js +1 -1
  25. package/dist/commands/pull.d.ts +2 -2
  26. package/dist/commands/pull.js +45 -47
  27. package/dist/commands/schema.d.ts +3 -3
  28. package/dist/commands/schema.js +37 -37
  29. package/dist/commands/sdk-key/create.js +12 -14
  30. package/dist/commands/sdk-key/revoke.d.ts +1 -1
  31. package/dist/commands/sdk-key/revoke.js +2 -4
  32. package/dist/commands/set-default.js +7 -5
  33. package/dist/commands/set-rollout.d.ts +1 -1
  34. package/dist/commands/set-rollout.js +16 -13
  35. package/dist/commands/sync.d.ts +3 -3
  36. package/dist/commands/sync.js +30 -26
  37. package/dist/commands/whoami.js +1 -5
  38. package/dist/commands/workspace/bootstrap.js +7 -7
  39. package/dist/commands/workspace/switch.js +2 -2
  40. package/dist/init/init-workspace.d.ts +2 -2
  41. package/dist/init/init-workspace.js +32 -18
  42. package/dist/init/samples.d.ts +1 -1
  43. package/dist/init/samples.js +2 -2
  44. package/dist/init/schema.js +24 -15
  45. package/dist/interactive-prompt.js +1 -10
  46. package/dist/migrate/doctor.js +3 -1
  47. package/dist/migrate/local-write.d.ts +26 -0
  48. package/dist/migrate/local-write.js +85 -0
  49. package/dist/migrate/migration-report.js +1 -9
  50. package/dist/migrate/push-strategy.js +1 -7
  51. package/dist/migrate/push-to-cloud.js +1 -1
  52. package/dist/migrate/sources/flagsmith.js +10 -0
  53. package/dist/quonfig.d.ts +1 -1
  54. package/dist/quonfig.js +5 -9
  55. package/dist/ui/get-value.js +1 -1
  56. package/dist/util/config-value-dto.js +17 -1
  57. package/dist/util/encryption.js +4 -1
  58. package/dist/util/friction-log.d.ts +1 -1
  59. package/dist/util/friction-log.js +5 -1
  60. package/dist/util/get-client.js +51 -3
  61. package/dist/util/get-valid-token.d.ts +4 -0
  62. package/dist/util/get-valid-token.js +17 -2
  63. package/dist/util/git-ops.js +7 -7
  64. package/dist/util/gitea-api.d.ts +2 -2
  65. package/dist/util/gitea-api.js +5 -3
  66. package/dist/util/gitea-token-storage.d.ts +2 -2
  67. package/dist/util/gitea-token-storage.js +1 -1
  68. package/dist/util/oauth-client.js +1 -1
  69. package/dist/util/token-storage.js +2 -2
  70. package/dist/verify/standalone.d.ts +0 -1
  71. package/dist/verify/standalone.js +22 -23
  72. package/dist/verify/validate.d.ts +8 -8
  73. package/dist/verify/validate.js +252 -246
  74. package/oclif.manifest.json +79 -3
  75. package/package.json +6 -6
@@ -0,0 +1,85 @@
1
+ import { execFile as execFileCb, spawn } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import * as util from 'node:util';
5
+ import { removeQfFromGitignore, writeImportState } from './import-state.js';
6
+ import { writeMigrationReport } from './migration-report.js';
7
+ import { MIGRATOR_IDENTITY } from './push-strategy.js';
8
+ const execFile = util.promisify(execFileCb);
9
+ const isGitRepo = async (dir) => {
10
+ try {
11
+ await execFile('git', ['-C', dir, 'rev-parse', '--git-dir']);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ };
18
+ const hasStagedChanges = async (dir) => {
19
+ const { stdout } = await execFile('git', ['-C', dir, 'status', '--porcelain']);
20
+ return stdout.trim().length > 0;
21
+ };
22
+ const gitCommitFromStdin = (cwd, message, env) => new Promise((resolve, reject) => {
23
+ const child = spawn('git', ['-C', cwd, 'commit', '-F', '-'], { env });
24
+ let stderr = '';
25
+ child.stderr.on('data', (d) => {
26
+ stderr += d.toString();
27
+ });
28
+ child.on('error', reject);
29
+ child.on('close', (code) => {
30
+ if (code === 0)
31
+ resolve();
32
+ else
33
+ reject(new Error(`git commit exited ${code}: ${stderr}`));
34
+ });
35
+ child.stdin.end(message);
36
+ });
37
+ const ensureQuonfigJson = (dir, environments) => {
38
+ const filePath = path.join(dir, 'quonfig.json');
39
+ if (fs.existsSync(filePath))
40
+ return;
41
+ const sorted = [...new Set(environments)].sort();
42
+ fs.writeFileSync(filePath, JSON.stringify({ environments: sorted }, null, 2) + '\n', 'utf8');
43
+ };
44
+ const ensureLocalRepo = async (localDir, branch) => {
45
+ fs.mkdirSync(localDir, { recursive: true });
46
+ if (await isGitRepo(localDir))
47
+ return 'reused';
48
+ await execFile('git', ['init', `--initial-branch=${branch}`, localDir]);
49
+ return 'initialized';
50
+ };
51
+ const writeQuonfigFiles = (dir, changes, source) => {
52
+ for (const change of changes) {
53
+ const files = source.translate(change);
54
+ for (const file of files) {
55
+ const full = path.join(dir, file.path);
56
+ fs.mkdirSync(path.dirname(full), { recursive: true });
57
+ fs.writeFileSync(full, file.contents);
58
+ }
59
+ }
60
+ };
61
+ export const applyLocalMigration = async (opts) => {
62
+ const branch = opts.branch ?? 'main';
63
+ const author = opts.author ?? MIGRATOR_IDENTITY;
64
+ const action = await ensureLocalRepo(opts.localDir, branch);
65
+ await execFile('git', ['-C', opts.localDir, 'config', 'user.name', author.name]);
66
+ await execFile('git', ['-C', opts.localDir, 'config', 'user.email', author.email]);
67
+ ensureQuonfigJson(opts.localDir, opts.environments);
68
+ writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
69
+ removeQfFromGitignore(opts.localDir);
70
+ writeImportState(opts.localDir, opts.importState);
71
+ writeMigrationReport(opts.localDir, opts.reportData);
72
+ await execFile('git', ['-C', opts.localDir, 'add', '--all']);
73
+ if (!(await hasStagedChanges(opts.localDir))) {
74
+ return { action, committed: false, commitSha: null };
75
+ }
76
+ await gitCommitFromStdin(opts.localDir, opts.commitMessage, {
77
+ ...process.env,
78
+ GIT_AUTHOR_EMAIL: author.email,
79
+ GIT_AUTHOR_NAME: author.name,
80
+ GIT_COMMITTER_EMAIL: author.email,
81
+ GIT_COMMITTER_NAME: author.name,
82
+ });
83
+ const { stdout: sha } = await execFile('git', ['-C', opts.localDir, 'rev-parse', 'HEAD']);
84
+ return { action, commitSha: sha.trim(), committed: true };
85
+ };
@@ -73,15 +73,7 @@ export const buildMigrationReport = (data) => {
73
73
  if (data.dryRun) {
74
74
  sections.push('> **DRY RUN** — no files were written to the workspace during this run.');
75
75
  }
76
- sections.push(`# Migration Report (from ${data.source})`);
77
- sections.push(`_Reflects only the changes produced by this run. Re-running overwrites this file._`);
78
- sections.push(renderCounts(data.counts));
79
- sections.push(renderCleanMappings(data.cleanMappings));
80
- sections.push(renderLossyMappings(data.lossyMappings));
81
- sections.push(renderUnsupported(data.unsupportedFeatures));
82
- sections.push(renderEnvironmentMap(data.environmentMap));
83
- sections.push(renderIdentifierMap(data.identifierMap));
84
- sections.push(renderFollowUp(data.followUp));
76
+ sections.push(`# Migration Report (from ${data.source})`, `_Reflects only the changes produced by this run. Re-running overwrites this file._`, renderCounts(data.counts), renderCleanMappings(data.cleanMappings), renderLossyMappings(data.lossyMappings), renderUnsupported(data.unsupportedFeatures), renderEnvironmentMap(data.environmentMap), renderIdentifierMap(data.identifierMap), renderFollowUp(data.followUp));
85
77
  return sections.join('\n\n') + '\n';
86
78
  };
87
79
  export const migrationReportPath = (outputDir) => path.join(outputDir, 'MIGRATION_REPORT.md');
@@ -82,13 +82,7 @@ const ensureCloneOrReuse = async (remoteUrl, localDir, branch) => {
82
82
  }
83
83
  }
84
84
  fs.mkdirSync(path.dirname(localDir), { recursive: true });
85
- await runGit(path.dirname(localDir) || '.', [
86
- 'clone',
87
- '--branch',
88
- branch,
89
- remoteUrl,
90
- localDir,
91
- ]);
85
+ await runGit(path.dirname(localDir) || '.', ['clone', '--branch', branch, remoteUrl, localDir]);
92
86
  return 'cloned';
93
87
  };
94
88
  const hasStagedChanges = async (dir) => {
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { removeQfFromGitignore, writeImportState } from './import-state.js';
4
4
  import { writeMigrationReport } from './migration-report.js';
5
- import { cloneAndStackPush, } from './push-strategy.js';
5
+ import { cloneAndStackPush } from './push-strategy.js';
6
6
  const writeQuonfigFiles = (dir, changes, source) => {
7
7
  for (const change of changes) {
8
8
  const files = source.translate(change);
@@ -1,3 +1,13 @@
1
+ // Stub — `qfg migrate --from flagsmith` is not yet wired up. Before replacing this with a real
2
+ // implementation, investigate:
3
+ // - Flagsmith API shape: what the admin/REST API returns for features, segments, identities,
4
+ // environments, and multivariate variations, and how that maps onto Quonfig's JSON schema.
5
+ // - Delta/cursor support: whether Flagsmith exposes an audit-log or change-feed endpoint with a
6
+ // stable cursor we can persist in `.qf/import-state.json` for re-runnable incremental
7
+ // imports, or whether we have to full-reimport + diff on every run (as is likely for LD).
8
+ // - Auth model: API key scopes (server-side vs. admin), org/project/environment hierarchy.
9
+ // - Schema gaps: Flagsmith's identity overrides and segment rules vs. Quonfig's rule operators
10
+ // — document any unsupported primitives the way `launchdarkly.README.md` does for LD.
1
11
  import { NotYetImplementedError } from '../source.js';
2
12
  const SOURCE_NAME = 'flagsmith';
3
13
  export const flagsmithSource = {
package/dist/quonfig.d.ts CHANGED
@@ -7,7 +7,7 @@ type Flags = {
7
7
  type FlagsOrDatafile = Flags | string;
8
8
  export declare const initQuonfig: (_ctx: CommandLike, flagsOrDatafile: FlagsOrDatafile) => Promise<Quonfig>;
9
9
  export declare const getQuonfig: () => Quonfig;
10
- export declare const overrideFor: ({ currentEnvironmentId, key, }: {
10
+ export declare const overrideFor: ({ currentEnvironmentId: _currentEnvironmentId, key, }: {
11
11
  currentEnvironmentId: string;
12
12
  key: string;
13
13
  }) => Value | undefined;
package/dist/quonfig.js CHANGED
@@ -32,16 +32,12 @@ export const getQuonfig = () => quonfig;
32
32
  * Get the raw ConfigResponse for a key (new quonfig JSON format).
33
33
  * The new format uses `default.rules` and `environment.rules` instead of `rows`.
34
34
  */
35
- const getRawConfig = (key) => {
36
- return quonfig.rawConfig?.(key);
37
- };
35
+ const getRawConfig = (key) => quonfig.rawConfig?.(key);
38
36
  /**
39
37
  * Find rules in an environment that target a specific user via criteria.
40
38
  */
41
- const getEnvironmentRules = (config) => {
42
- return config.environment?.rules;
43
- };
44
- export const overrideFor = ({ currentEnvironmentId, key, }) => {
39
+ const getEnvironmentRules = (config) => config.environment?.rules;
40
+ export const overrideFor = ({ currentEnvironmentId: _currentEnvironmentId, key, }) => {
45
41
  const config = getRawConfig(key);
46
42
  if (!config)
47
43
  return undefined;
@@ -65,11 +61,11 @@ export const defaultValueFor = (_envId, key) => {
65
61
  // In the new format, the default value is the last rule in environment rules, or default rules
66
62
  const envRules = config.environment?.rules;
67
63
  if (envRules && envRules.length > 0) {
68
- return envRules[envRules.length - 1]?.value;
64
+ return envRules.at(-1)?.value;
69
65
  }
70
66
  const defaultRules = config.default?.rules;
71
67
  if (defaultRules && defaultRules.length > 0) {
72
- return defaultRules[defaultRules.length - 1]?.value;
68
+ return defaultRules.at(-1)?.value;
73
69
  }
74
70
  return undefined;
75
71
  };
@@ -38,7 +38,7 @@ const valueToString = (v) => {
38
38
  return '';
39
39
  return String(v.value);
40
40
  };
41
- const promptForValue = async ({ allowBlank, config, currentDefault, message, }) => {
41
+ const promptForValue = async ({ allowBlank, config: _config, currentDefault, message, }) => {
42
42
  // In the new quonfig format, allowable values aren't stored at the config level.
43
43
  // Just prompt for a string input.
44
44
  if (!currentDefault) {
@@ -38,7 +38,23 @@ export function mapConfigValueToDto(configValue, valueType) {
38
38
  value = configValue.stringList.values;
39
39
  }
40
40
  else if (configValue.json !== undefined) {
41
- value = configValue.json;
41
+ // ConfigValue.json is wrapped as {json: "<raw JSON string>"} (see coerceIntoType).
42
+ // The API expects the parsed JSON value, not the wrapper or the raw string —
43
+ // otherwise the server stores the default as {} (see qfg-c0q).
44
+ const rawJson = configValue.json.json;
45
+ if (typeof rawJson === 'string') {
46
+ try {
47
+ value = JSON.parse(rawJson);
48
+ }
49
+ catch {
50
+ // Fall back to raw string if somehow it isn't valid JSON — coerceIntoType
51
+ // should have already rejected this case, so this is defensive only.
52
+ value = rawJson;
53
+ }
54
+ }
55
+ else {
56
+ value = rawJson ?? configValue.json;
57
+ }
42
58
  }
43
59
  else if (configValue.duration !== undefined) {
44
60
  value = configValue.duration;
@@ -72,7 +72,10 @@ export async function makeConfidentialValue(command, value, secret, environmentI
72
72
  });
73
73
  }
74
74
  // Fetch the encryption key config
75
- const configRequest = await command.apiClient.post('/api/v1/metadata/getByKey', { workspaceId: command.workspaceId, key: secret.keyName });
75
+ const configRequest = await command.apiClient.post('/api/v1/metadata/getByKey', {
76
+ workspaceId: command.workspaceId,
77
+ key: secret.keyName,
78
+ });
76
79
  if (!configRequest.ok) {
77
80
  const message = configRequest.status === 404
78
81
  ? `Failed to create secret: ${secret.keyName} not found`
@@ -4,7 +4,7 @@ export interface FrictionEntry {
4
4
  exitCode: number;
5
5
  ts: string;
6
6
  }
7
- export declare function getFrictionLogPath(envValue: string | undefined, cwd?: string): null | string;
7
+ export declare function getFrictionLogPath(envValue?: string | undefined, cwd?: string): null | string;
8
8
  export declare function buildFrictionEntry(opts: {
9
9
  argv?: string[];
10
10
  binName?: string;
@@ -29,8 +29,12 @@ const MAX_STDERR_CAPTURE = 4000;
29
29
  export function extractLastErrorLine(stderr) {
30
30
  if (!stderr)
31
31
  return undefined;
32
+ // eslint-disable-next-line no-control-regex
32
33
  const stripped = stderr.replaceAll(/\u001B\[[\d;]*[A-Za-z]/g, '');
33
- const lines = stripped.split('\n').map((l) => l.trim()).filter(Boolean);
34
+ const lines = stripped
35
+ .split('\n')
36
+ .map((l) => l.trim())
37
+ .filter(Boolean);
34
38
  if (lines.length === 0)
35
39
  return undefined;
36
40
  const errorLine = [...lines].reverse().find((l) => /^(error|»|×|✖)/i.test(l));
@@ -20,10 +20,58 @@ const getClient = async (command, sdkKey, profile) => {
20
20
  }
21
21
  let jwt;
22
22
  let workspaceId;
23
- // If no SDK key provided, use WorkOS OAuth tokens
24
- if (!sdkKey) {
23
+ const apiKey = process.env.QUONFIG_API_KEY;
24
+ // Auth precedence: explicit SDK key → QUONFIG_API_KEY env (CI) → WorkOS OAuth tokens.
25
+ if (sdkKey) {
26
+ // No jwt / workspace resolution — SDK-key path talks to api-delivery.
27
+ }
28
+ else if (apiKey && apiKey.length > 0) {
29
+ // API-key (workspace-scoped bearer) path. getValidAccessToken short-circuits
30
+ // when QUONFIG_API_KEY is set, so this never touches disk.
31
+ try {
32
+ jwt = await getValidAccessToken();
33
+ }
34
+ catch (error) {
35
+ command.error(error instanceof Error ? error.message : String(error), { exit: 1 });
36
+ }
37
+ const workspaceOverride = profile ?? process.env.QUONFIG_WORKSPACE;
38
+ if (!workspaceOverride) {
39
+ command.error('QUONFIG_API_KEY is set but no workspace was specified. Set QUONFIG_WORKSPACE=<slug-or-uuid> or pass --workspace=<slug-or-uuid>.', { exit: 1 });
40
+ }
41
+ const uuidPattern = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i;
42
+ if (uuidPattern.test(workspaceOverride)) {
43
+ workspaceId = workspaceOverride;
44
+ command.verboseLog('ApiKey auth', { source: 'uuid', workspaceId });
45
+ }
46
+ else {
47
+ let res;
48
+ try {
49
+ res = await fetch(`${getApiUrl()}/api/v1/userWorkspaces/list`, {
50
+ method: 'POST',
51
+ headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({ json: {} }),
53
+ });
54
+ }
55
+ catch (error) {
56
+ command.error(`Failed to resolve workspace slug "${workspaceOverride}": ${error instanceof Error ? error.message : String(error)}`, { exit: 1 });
57
+ }
58
+ if (!res.ok) {
59
+ command.error(`Failed to resolve workspace slug "${workspaceOverride}" (HTTP ${res.status}). Check that QUONFIG_API_KEY is valid.`, { exit: 1 });
60
+ }
61
+ const body = (await res.json());
62
+ const entries = body.json ?? body ?? [];
63
+ const match = entries.find((w) => w.workspaceSlug === workspaceOverride);
64
+ if (!match) {
65
+ const available = entries.map((w) => w.workspaceSlug).join(', ') || '(none)';
66
+ command.error(`No workspace with slug "${workspaceOverride}" is accessible with this API key. Available: ${available}.`, { exit: 1 });
67
+ }
68
+ workspaceId = match.workspaceId;
69
+ command.verboseLog('ApiKey auth', { source: 'slug', slug: workspaceOverride, workspaceId });
70
+ }
71
+ }
72
+ else {
25
73
  const authConfig = await loadAuthConfig();
26
- let tokens = await loadTokens();
74
+ const tokens = await loadTokens();
27
75
  command.verboseLog('OAuth auth', {
28
76
  hasAuthConfig: Boolean(authConfig),
29
77
  hasAccessToken: Boolean(tokens?.accessToken),
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Returns a valid (non-expired) WorkOS access token, refreshing if needed.
3
3
  * Throws if not logged in or if refresh fails.
4
+ *
5
+ * If `QUONFIG_API_KEY` is set in the environment (non-empty), it is used
6
+ * directly as a bearer token and no disk reads/writes or refresh calls
7
+ * are made. This is the non-interactive (CI) path.
4
8
  */
5
9
  export declare function getValidAccessToken(): Promise<string>;
@@ -1,10 +1,21 @@
1
1
  /**
2
2
  * Returns a valid (non-expired) WorkOS access token, refreshing if needed.
3
3
  * Throws if not logged in or if refresh fails.
4
+ *
5
+ * If `QUONFIG_API_KEY` is set in the environment (non-empty), it is used
6
+ * directly as a bearer token and no disk reads/writes or refresh calls
7
+ * are made. This is the non-interactive (CI) path.
4
8
  */
5
9
  import { decodeJWT, refreshAccessToken } from './oauth-client.js';
6
10
  import { loadTokens, saveTokens } from './token-storage.js';
7
11
  export async function getValidAccessToken() {
12
+ const envKey = process.env.QUONFIG_API_KEY;
13
+ if (envKey && envKey.length > 0) {
14
+ if (!envKey.startsWith('qf_uk_')) {
15
+ throw new Error(`QUONFIG_API_KEY must start with "qf_uk_" (got "${envKey.slice(0, 8)}..."). Generate one from the web UI under Settings → API Keys.`);
16
+ }
17
+ return envKey;
18
+ }
8
19
  const tokens = await loadTokens();
9
20
  if (!tokens?.accessToken) {
10
21
  throw new Error('Not authenticated. Please run `qfg login` first.');
@@ -17,7 +28,9 @@ export async function getValidAccessToken() {
17
28
  expired = payload.exp * 1000 < Date.now();
18
29
  }
19
30
  }
20
- catch { /* use stored expiresAt */ }
31
+ catch {
32
+ /* use stored expiresAt */
33
+ }
21
34
  if (!expired) {
22
35
  return tokens.accessToken;
23
36
  }
@@ -32,7 +45,9 @@ export async function getValidAccessToken() {
32
45
  expiresAt = payload.exp * 1000;
33
46
  }
34
47
  }
35
- catch { /* use fallback */ }
48
+ catch {
49
+ /* use fallback */
50
+ }
36
51
  const updated = {
37
52
  ...tokens,
38
53
  accessToken: refreshed.access_token,
@@ -4,7 +4,7 @@ const execFile = util.promisify(execFileCb);
4
4
  /**
5
5
  * Redact a token from a URL string so it is safe to display to users.
6
6
  */
7
- export const redactToken = (url) => url.replace(/:([^@/]+)@/, ':***@');
7
+ export const redactToken = (url) => url.replace(/:([^/@]+)@/, ':***@');
8
8
  /**
9
9
  * Wrap execFile errors to strip tokens from the message.
10
10
  */
@@ -12,9 +12,9 @@ const safeExec = async (file, args, options) => {
12
12
  try {
13
13
  return await execFile(file, args, { cwd: options?.cwd });
14
14
  }
15
- catch (err) {
16
- const e = err;
17
- const message = redactToken(e.message || String(err));
15
+ catch (error) {
16
+ const e = error;
17
+ const message = redactToken(e.message || String(error));
18
18
  const stderr = redactToken(e.stderr ?? '');
19
19
  const safeErr = new Error(message);
20
20
  safeErr.stderr = stderr;
@@ -136,11 +136,11 @@ export const gitMergeFfOnly = async (dir) => {
136
136
  */
137
137
  export const gitSetRemote = async (dir, url) => {
138
138
  const existing = await getRemoteUrl(dir);
139
- if (existing !== null) {
140
- await safeExec('git', ['-C', dir, 'remote', 'set-url', 'origin', url]);
139
+ if (existing === null) {
140
+ await safeExec('git', ['-C', dir, 'remote', 'add', 'origin', url]);
141
141
  }
142
142
  else {
143
- await safeExec('git', ['-C', dir, 'remote', 'add', 'origin', url]);
143
+ await safeExec('git', ['-C', dir, 'remote', 'set-url', 'origin', url]);
144
144
  }
145
145
  };
146
146
  export const gitPushForceLease = async (dir) => {
@@ -1,8 +1,8 @@
1
1
  import { GiteaTokenEntry } from './gitea-token-storage.js';
2
2
  export interface GiteaTokenResponse {
3
- token: string;
4
- repoUrl: string;
5
3
  expiresAt: string | null;
4
+ repoUrl: string;
5
+ token: string;
6
6
  }
7
7
  export declare const mintGiteaToken: (workspaceId: string, scope: "read" | "write", purpose: "pull" | "bootstrap") => Promise<GiteaTokenResponse>;
8
8
  /**
@@ -7,7 +7,7 @@ export const mintGiteaToken = async (workspaceId, scope, purpose) => {
7
7
  const res = await fetch(`${apiUrl}/api/v1/gitea/token`, {
8
8
  method: 'POST',
9
9
  headers: {
10
- 'Authorization': `Bearer ${accessToken}`,
10
+ Authorization: `Bearer ${accessToken}`,
11
11
  'Content-Type': 'application/json',
12
12
  },
13
13
  body: JSON.stringify({ json: { scope, workspaceId, purpose } }),
@@ -23,10 +23,12 @@ export const mintGiteaToken = async (workspaceId, scope, purpose) => {
23
23
  else if (body?.message)
24
24
  message = body.message;
25
25
  }
26
- catch { /* use raw text */ }
26
+ catch {
27
+ /* use raw text */
28
+ }
27
29
  throw new Error(message || text);
28
30
  }
29
- const body = await res.json();
31
+ const body = (await res.json());
30
32
  const data = body.json ?? body;
31
33
  return data;
32
34
  };
@@ -1,7 +1,7 @@
1
1
  export interface GiteaTokenEntry {
2
- token: string;
3
- repoUrl: string;
4
2
  expiresAt: string | null;
3
+ repoUrl: string;
4
+ token: string;
5
5
  }
6
6
  export declare const loadGiteaToken: (workspaceId: string) => Promise<GiteaTokenEntry | null>;
7
7
  export declare const saveGiteaToken: (workspaceId: string, entry: GiteaTokenEntry) => Promise<void>;
@@ -6,7 +6,7 @@ const BUFFER_MS = 5 * 60 * 1000; // 5 minutes
6
6
  const getQuonfigDir = () => path.join(os.homedir(), '.quonfig');
7
7
  const getGiteaTokenFile = () => {
8
8
  const domain = getDomain();
9
- const filename = domain !== 'quonfig.com' ? `gitea-tokens-${domain.replaceAll('.', '-')}.json` : 'gitea-tokens.json';
9
+ const filename = domain === 'quonfig.com' ? 'gitea-tokens.json' : `gitea-tokens-${domain.replaceAll('.', '-')}.json`;
10
10
  return path.join(getQuonfigDir(), filename);
11
11
  };
12
12
  const ensureQuonfigDir = async () => {
@@ -53,7 +53,7 @@ export const pollForToken = async (deviceCode, interval, expiresIn, verbose) =>
53
53
  return response.json();
54
54
  }
55
55
  // eslint-disable-next-line no-await-in-loop
56
- const error = await response.json().catch(() => ({}));
56
+ const error = (await response.json().catch(() => ({})));
57
57
  const errorCode = error.error || error.code;
58
58
  if (errorCode === 'authorization_pending') {
59
59
  continue;
@@ -5,7 +5,7 @@ import { getDomain } from './domain-urls.js';
5
5
  const getQuonfigDir = (options) => options?.quonfigDir || path.join(os.homedir(), '.quonfig');
6
6
  const getTokenFile = (options) => {
7
7
  const domain = getDomain();
8
- const filename = domain !== 'quonfig.com' ? `tokens-${domain.replaceAll('.', '-')}.json` : 'tokens.json';
8
+ const filename = domain === 'quonfig.com' ? 'tokens.json' : `tokens-${domain.replaceAll('.', '-')}.json`;
9
9
  return path.join(getQuonfigDir(options), filename);
10
10
  };
11
11
  const getConfigFile = (options) => path.join(getQuonfigDir(options), 'config');
@@ -86,7 +86,7 @@ export const loadAuthConfig = async (options) => {
86
86
  config.defaultProfile = defaultMatch[1].trim();
87
87
  }
88
88
  // Parse profiles — each block starts with [profile name] and contains key=value lines
89
- const profileBlockRegex = /\[profile\s+(\w+)]\s*\n((?:[^\[]*\n?)*)/g;
89
+ const profileBlockRegex = /\[profile\s+(\w+)]\s*\n((?:[^[]*\n?)*)/g;
90
90
  let match;
91
91
  while ((match = profileBlockRegex.exec(data)) !== null) {
92
92
  const profileName = match[1];
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * Standalone workspace validator.
4
3
  *
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * Standalone workspace validator.
4
3
  *
@@ -11,19 +10,19 @@
11
10
  * 0 All checks pass
12
11
  * 1 Validation errors found
13
12
  */
14
- import { execSync } from "node:child_process";
15
- import * as readline from "node:readline";
16
- import { formatResult, validateFileMap, validateWorkspace } from "./validate.js";
13
+ import { execSync } from 'node:child_process';
14
+ import * as readline from 'node:readline';
15
+ import { formatResult, validateFileMap, validateWorkspace } from './validate.js';
17
16
  async function main() {
18
17
  const args = process.argv.slice(2);
19
- if (args.includes("--git-hook")) {
18
+ if (args.includes('--git-hook')) {
20
19
  await runGitHook();
21
20
  }
22
- else if (args.includes("--help") || args.includes("-h")) {
21
+ else if (args.includes('--help') || args.includes('-h')) {
23
22
  printUsage();
24
23
  }
25
24
  else {
26
- const dir = args[0] || ".";
25
+ const dir = args[0] || '.';
27
26
  runDiskValidation(dir);
28
27
  }
29
28
  }
@@ -55,23 +54,23 @@ async function runGitHook() {
55
54
  // Read all of stdin (ref lines). Use callback API for bun compatibility.
56
55
  await new Promise((resolve) => {
57
56
  const rl = readline.createInterface({ input: process.stdin });
58
- rl.on("line", (line) => {
59
- const parts = line.trim().split(" ");
57
+ rl.on('line', (line) => {
58
+ const parts = line.trim().split(' ');
60
59
  if (parts.length >= 3) {
61
60
  refs.push({ oldOid: parts[0], newOid: parts[1], refName: parts[2] });
62
61
  }
63
62
  });
64
- rl.on("close", resolve);
63
+ rl.on('close', resolve);
65
64
  });
66
65
  if (refs.length === 0) {
67
- console.log("qfg-verify: no refs received");
66
+ console.log('qfg-verify: no refs received');
68
67
  process.exit(0);
69
68
  }
70
69
  // Validate the latest pushed commit for each ref
71
70
  let hasErrors = false;
72
71
  for (const ref of refs) {
73
72
  // Skip deletions
74
- if (ref.newOid === "0000000000000000000000000000000000000000")
73
+ if (ref.newOid === '0000000000000000000000000000000000000000')
75
74
  continue;
76
75
  console.log(`qfg-verify: validating ${ref.refName} (${ref.newOid.slice(0, 8)})`);
77
76
  try {
@@ -82,8 +81,8 @@ async function runGitHook() {
82
81
  hasErrors = true;
83
82
  }
84
83
  }
85
- catch (err) {
86
- console.error(`qfg-verify: error reading commit ${ref.newOid}: ${err.message}`);
84
+ catch (error) {
85
+ console.error(`qfg-verify: error reading commit ${ref.newOid}: ${error.message}`);
87
86
  hasErrors = true;
88
87
  }
89
88
  }
@@ -95,28 +94,28 @@ async function runGitHook() {
95
94
  */
96
95
  function readFilesFromCommit(commitOid) {
97
96
  const files = new Map();
98
- const dirs = ["configs", "feature-flags", "segments", "log-levels", "schemas", "schemas-protected"];
97
+ const dirs = ['configs', 'feature-flags', 'segments', 'log-levels', 'schemas', 'schemas-protected'];
99
98
  for (const dir of dirs) {
100
99
  // List files in this directory at the given commit
101
100
  let listing;
102
101
  try {
103
102
  listing = execSync(`git ls-tree --name-only ${commitOid} ${dir}/`, {
104
- encoding: "utf-8",
105
- stdio: ["pipe", "pipe", "pipe"],
103
+ encoding: 'utf8',
104
+ stdio: ['pipe', 'pipe', 'pipe'],
106
105
  });
107
106
  }
108
107
  catch {
109
108
  // Directory doesn't exist in this commit
110
109
  continue;
111
110
  }
112
- const filenames = listing.trim().split("\n").filter(Boolean);
111
+ const filenames = listing.trim().split('\n').filter(Boolean);
113
112
  for (const filePath of filenames) {
114
- if (!filePath.endsWith(".json") || filePath.includes("/."))
113
+ if (!filePath.endsWith('.json') || filePath.includes('/.'))
115
114
  continue;
116
115
  try {
117
116
  const content = execSync(`git show ${commitOid}:${filePath}`, {
118
- encoding: "utf-8",
119
- stdio: ["pipe", "pipe", "pipe"],
117
+ encoding: 'utf8',
118
+ stdio: ['pipe', 'pipe', 'pipe'],
120
119
  });
121
120
  files.set(filePath, content);
122
121
  }
@@ -127,7 +126,7 @@ function readFilesFromCommit(commitOid) {
127
126
  }
128
127
  return files;
129
128
  }
130
- main().catch((err) => {
131
- console.error(`qfg-verify: fatal: ${err.message}`);
129
+ main().catch((error) => {
130
+ console.error(`qfg-verify: fatal: ${error.message}`);
132
131
  process.exit(1);
133
132
  });