@quonfig/cli 0.0.11 → 0.0.13
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/README.md +214 -30
- package/dist/codegen/json-schema-to-zod.js +7 -5
- package/dist/codegen/language-mappers/zod-base-mapper.d.ts +2 -2
- package/dist/codegen/language-mappers/zod-to-string-mapper.d.ts +2 -2
- package/dist/codegen/language-mappers/zod-to-string-mapper.js +6 -6
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.d.ts +2 -2
- package/dist/codegen/language-mappers/zod-to-typescript-mapper.js +6 -6
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.d.ts +2 -2
- package/dist/codegen/language-mappers/zod-to-typescript-return-value-mapper.js +5 -5
- package/dist/codegen/local-config-reader.js +0 -1
- package/dist/codegen/schema-extractor.js +5 -2
- package/dist/commands/info.js +4 -1
- package/dist/commands/init.js +12 -2
- package/dist/commands/interactive.d.ts +1 -0
- package/dist/commands/interactive.js +8 -0
- package/dist/commands/login.js +9 -3
- package/dist/commands/migrate/doctor.d.ts +12 -0
- package/dist/commands/migrate/doctor.js +63 -0
- package/dist/commands/migrate/my-code.d.ts +11 -0
- package/dist/commands/migrate/my-code.js +80 -0
- package/dist/commands/migrate/status.d.ts +10 -0
- package/dist/commands/migrate/status.js +99 -0
- package/dist/commands/migrate.d.ts +20 -0
- package/dist/commands/migrate.js +277 -0
- package/dist/commands/override.js +6 -3
- package/dist/commands/profile.d.ts +1 -1
- package/dist/commands/profile.js +1 -1
- package/dist/commands/pull.d.ts +2 -2
- package/dist/commands/pull.js +38 -41
- package/dist/commands/schema.d.ts +3 -3
- package/dist/commands/schema.js +37 -37
- package/dist/commands/sdk-key/create.js +12 -14
- package/dist/commands/sdk-key/revoke.d.ts +1 -1
- package/dist/commands/sdk-key/revoke.js +2 -4
- package/dist/commands/set-default.js +7 -5
- package/dist/commands/set-rollout.d.ts +1 -1
- package/dist/commands/set-rollout.js +16 -13
- package/dist/commands/sync.d.ts +3 -3
- package/dist/commands/sync.js +30 -26
- package/dist/commands/whoami.js +1 -5
- package/dist/commands/workspace/bootstrap.js +7 -7
- package/dist/commands/workspace/switch.js +2 -2
- package/dist/init/init-workspace.d.ts +2 -2
- package/dist/init/init-workspace.js +32 -18
- package/dist/init/samples.d.ts +1 -1
- package/dist/init/samples.js +2 -2
- package/dist/init/schema.js +24 -15
- package/dist/interactive-prompt.js +1 -10
- package/dist/migrate/doctor.d.ts +27 -0
- package/dist/migrate/doctor.js +222 -0
- package/dist/migrate/identifier-map.d.ts +14 -0
- package/dist/migrate/identifier-map.js +55 -0
- package/dist/migrate/import-state.d.ts +14 -0
- package/dist/migrate/import-state.js +60 -0
- package/dist/migrate/local-write.d.ts +26 -0
- package/dist/migrate/local-write.js +85 -0
- package/dist/migrate/migration-report.d.ts +42 -0
- package/dist/migrate/migration-report.js +85 -0
- package/dist/migrate/push-strategy.d.ts +34 -0
- package/dist/migrate/push-strategy.js +127 -0
- package/dist/migrate/push-to-cloud.d.ts +15 -0
- package/dist/migrate/push-to-cloud.js +31 -0
- package/dist/migrate/registry.d.ts +6 -0
- package/dist/migrate/registry.js +25 -0
- package/dist/migrate/source.d.ts +22 -0
- package/dist/migrate/source.js +12 -0
- package/dist/migrate/sources/flagsmith.d.ts +2 -0
- package/dist/migrate/sources/flagsmith.js +28 -0
- package/dist/migrate/sources/launch/api.d.ts +7 -0
- package/dist/migrate/sources/launch/api.js +75 -0
- package/dist/migrate/sources/launch/translate.d.ts +6 -0
- package/dist/migrate/sources/launch/translate.js +190 -0
- package/dist/migrate/sources/launch/types.d.ts +68 -0
- package/dist/migrate/sources/launch/types.js +1 -0
- package/dist/migrate/sources/launch.d.ts +3 -0
- package/dist/migrate/sources/launch.js +70 -0
- package/dist/migrate/sources/launchdarkly.d.ts +2 -0
- package/dist/migrate/sources/launchdarkly.js +18 -0
- package/dist/quonfig.d.ts +1 -1
- package/dist/quonfig.js +5 -9
- package/dist/ui/get-confirmation.d.ts +2 -1
- package/dist/ui/get-confirmation.js +9 -1
- package/dist/ui/get-value.d.ts +1 -1
- package/dist/ui/get-value.js +4 -3
- package/dist/util/config-value-dto.js +17 -1
- package/dist/util/encryption.js +4 -1
- package/dist/util/friction-log.d.ts +1 -1
- package/dist/util/friction-log.js +5 -1
- package/dist/util/get-client.js +51 -3
- package/dist/util/get-valid-token.d.ts +4 -0
- package/dist/util/get-valid-token.js +17 -2
- package/dist/util/git-ops.js +7 -7
- package/dist/util/gitea-api.d.ts +2 -2
- package/dist/util/gitea-api.js +5 -3
- package/dist/util/gitea-token-storage.d.ts +2 -2
- package/dist/util/gitea-token-storage.js +1 -1
- package/dist/util/is-interactive.d.ts +21 -1
- package/dist/util/is-interactive.js +21 -1
- package/dist/util/oauth-client.js +1 -1
- package/dist/util/token-storage.js +2 -2
- package/dist/verify/standalone.d.ts +0 -1
- package/dist/verify/standalone.js +22 -23
- package/dist/verify/validate.d.ts +8 -8
- package/dist/verify/validate.js +256 -245
- package/oclif.manifest.json +365 -3
- package/package.json +6 -6
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { BaseCommand } from '../index.js';
|
|
5
|
+
import { CrossSourceError, assertSourceMatches, readImportState } from '../migrate/import-state.js';
|
|
6
|
+
import { applyLocalMigration } from '../migrate/local-write.js';
|
|
7
|
+
import { PushConflictError } from '../migrate/push-strategy.js';
|
|
8
|
+
import { pushMigrationToCloud } from '../migrate/push-to-cloud.js';
|
|
9
|
+
import { UnknownSourceError, getSource } from '../migrate/registry.js';
|
|
10
|
+
import { NotYetImplementedError } from '../migrate/source.js';
|
|
11
|
+
import { applyLaunchBaseUrl } from '../migrate/sources/launch/api.js';
|
|
12
|
+
import { mintGiteaToken } from '../util/gitea-api.js';
|
|
13
|
+
import { displayUrl } from '../util/git-ops.js';
|
|
14
|
+
import { getActiveProfile, loadAuthConfig, resolveWorkspaceId } from '../util/token-storage.js';
|
|
15
|
+
const DEFAULT_DIR = 'quonfig-config';
|
|
16
|
+
export default class Migrate extends BaseCommand {
|
|
17
|
+
static description = 'Migrate flags and configs from a legacy feature-flag system into a Quonfig workspace.\n\n' +
|
|
18
|
+
'Currently supported sources:\n' +
|
|
19
|
+
' launch — Reforge Launch (ported from launch-migrator)\n\n' +
|
|
20
|
+
'See https://docs.quonfig.com/docs/migrating/from-launch for the step-by-step guide,\n' +
|
|
21
|
+
'and https://docs.quonfig.com/docs/migrating/troubleshooting when something goes sideways.';
|
|
22
|
+
static examples = [
|
|
23
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-config',
|
|
24
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --workspace acme-prod --push',
|
|
25
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-config --reset',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-config --dry-run',
|
|
27
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-config --staging',
|
|
28
|
+
];
|
|
29
|
+
static flags = {
|
|
30
|
+
'api-key': Flags.string({
|
|
31
|
+
description: 'API key for the legacy source (or set LAUNCH_API_KEY)',
|
|
32
|
+
env: 'LAUNCH_API_KEY',
|
|
33
|
+
}),
|
|
34
|
+
dir: Flags.string({
|
|
35
|
+
description: 'Target local workspace directory. Defaults to cwd if it looks like a Quonfig workspace, otherwise ./quonfig-config.',
|
|
36
|
+
}),
|
|
37
|
+
'dry-run': Flags.boolean({
|
|
38
|
+
default: false,
|
|
39
|
+
description: 'Fetch and summarize changes without writing anything',
|
|
40
|
+
}),
|
|
41
|
+
from: Flags.string({
|
|
42
|
+
description: 'Legacy source to migrate from',
|
|
43
|
+
options: ['launch', 'launchdarkly', 'flagsmith'],
|
|
44
|
+
required: true,
|
|
45
|
+
}),
|
|
46
|
+
push: Flags.boolean({
|
|
47
|
+
default: false,
|
|
48
|
+
description: 'After migrating to a local dir, also push to the given --workspace on Quonfig cloud',
|
|
49
|
+
}),
|
|
50
|
+
recent: Flags.integer({
|
|
51
|
+
description: 'Import only the last N changes (useful for tire-kicking)',
|
|
52
|
+
}),
|
|
53
|
+
reset: Flags.boolean({
|
|
54
|
+
default: false,
|
|
55
|
+
description: 'Ignore the delta cursor and re-import everything from scratch',
|
|
56
|
+
}),
|
|
57
|
+
since: Flags.string({
|
|
58
|
+
description: 'Override the delta cursor (epoch milliseconds or ISO-8601 timestamp)',
|
|
59
|
+
}),
|
|
60
|
+
staging: Flags.boolean({
|
|
61
|
+
default: false,
|
|
62
|
+
description: 'Hit the staging API for the source (dev-only)',
|
|
63
|
+
}),
|
|
64
|
+
workspace: Flags.string({
|
|
65
|
+
description: 'Quonfig cloud workspace slug to push to. Requires `qfg login` and is typically combined with --push.',
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
async run() {
|
|
69
|
+
const { flags } = await this.parse(Migrate);
|
|
70
|
+
if (flags.dir && flags.workspace && !flags.push) {
|
|
71
|
+
return this.err('`--dir` and `--workspace` are mutually exclusive unless `--push` is also passed. ' +
|
|
72
|
+
'Pass `--push` to migrate locally and then push to the cloud workspace, or drop one of the flags.');
|
|
73
|
+
}
|
|
74
|
+
if (!flags['api-key']) {
|
|
75
|
+
return this.err('--api-key is required. For --from launch you can also set LAUNCH_API_KEY in your environment.');
|
|
76
|
+
}
|
|
77
|
+
let workspaceContext = null;
|
|
78
|
+
if (flags.push) {
|
|
79
|
+
const resolved = await this.resolvePushWorkspace(flags.workspace);
|
|
80
|
+
if (typeof resolved === 'string')
|
|
81
|
+
return this.err(resolved);
|
|
82
|
+
workspaceContext = resolved;
|
|
83
|
+
}
|
|
84
|
+
const dir = resolveTargetDir(flags.dir, process.cwd());
|
|
85
|
+
if (!flags.reset) {
|
|
86
|
+
try {
|
|
87
|
+
assertSourceMatches(dir, flags.from);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof CrossSourceError)
|
|
91
|
+
return this.err(error.message);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (flags.from === 'launch') {
|
|
96
|
+
applyLaunchBaseUrl(flags.staging);
|
|
97
|
+
}
|
|
98
|
+
let source;
|
|
99
|
+
try {
|
|
100
|
+
source = getSource(flags.from);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof UnknownSourceError)
|
|
104
|
+
return this.err(error.message);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await source.validateAuth(flags['api-key']);
|
|
109
|
+
const environments = await source.listEnvironments();
|
|
110
|
+
const existing = readImportState(dir);
|
|
111
|
+
const sinceEpochMs = computeSince(flags, existing);
|
|
112
|
+
const changes = [];
|
|
113
|
+
for await (const change of source.fetchChanges(sinceEpochMs)) {
|
|
114
|
+
changes.push(change);
|
|
115
|
+
}
|
|
116
|
+
const toProcess = flags.recent === undefined ? changes : changes.slice(-flags.recent);
|
|
117
|
+
const payload = {
|
|
118
|
+
dir,
|
|
119
|
+
dryRun: flags['dry-run'],
|
|
120
|
+
fetched: changes.length,
|
|
121
|
+
from: flags.from,
|
|
122
|
+
processed: toProcess.length,
|
|
123
|
+
since: sinceEpochMs,
|
|
124
|
+
};
|
|
125
|
+
if (flags['dry-run']) {
|
|
126
|
+
this.log(`Dry run: would migrate ${toProcess.length} change(s) from ${flags.from} into ${dir}.`);
|
|
127
|
+
return payload;
|
|
128
|
+
}
|
|
129
|
+
if (flags.push && workspaceContext) {
|
|
130
|
+
const { workspaceId } = workspaceContext;
|
|
131
|
+
this.log('Connecting to Gitea...');
|
|
132
|
+
let repoUrl;
|
|
133
|
+
try {
|
|
134
|
+
const tokenData = await mintGiteaToken(workspaceId, 'write', 'bootstrap');
|
|
135
|
+
repoUrl = tokenData.repoUrl;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return this.err(`Could not get Gitea credentials for workspace ${workspaceId}: ${String(error)}\n` +
|
|
139
|
+
'Make sure the workspace is fully provisioned in the Quonfig app before pushing.');
|
|
140
|
+
}
|
|
141
|
+
this.verboseLog('Migrate', { repoUrl: displayUrl(repoUrl) });
|
|
142
|
+
const latestChangedAt = latestChangedAtOf(toProcess, sinceEpochMs ?? undefined);
|
|
143
|
+
const importState = { source: flags.from };
|
|
144
|
+
if (latestChangedAt !== undefined)
|
|
145
|
+
importState.lastProcessedAt = latestChangedAt;
|
|
146
|
+
this.log(`Pushing ${toProcess.length} change(s) to workspace ${workspaceId} (clone-and-stack)...`);
|
|
147
|
+
try {
|
|
148
|
+
const result = await pushMigrationToCloud({
|
|
149
|
+
changes: toProcess,
|
|
150
|
+
commitMessage: buildCommitMessage(flags.from, toProcess.length),
|
|
151
|
+
importState,
|
|
152
|
+
localDir: dir,
|
|
153
|
+
remoteUrl: repoUrl,
|
|
154
|
+
reportData: buildReportData(flags.from, toProcess.length),
|
|
155
|
+
source,
|
|
156
|
+
});
|
|
157
|
+
this.log(result.committed
|
|
158
|
+
? `Pushed ${toProcess.length} change(s). commit=${result.commitSha?.slice(0, 8) ?? ''} action=${result.action}`
|
|
159
|
+
: `No net changes produced by this run. Nothing to commit or push.`);
|
|
160
|
+
return {
|
|
161
|
+
...payload,
|
|
162
|
+
action: result.action,
|
|
163
|
+
commitSha: result.commitSha,
|
|
164
|
+
committed: result.committed,
|
|
165
|
+
pushed: true,
|
|
166
|
+
workspaceId,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof PushConflictError) {
|
|
171
|
+
return this.err(`${error.message}\n\nRe-run \`qfg migrate --from ${flags.from} --workspace ${workspaceId} --push\` to pick up remote changes before retrying.`);
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const latestLocalChangedAt = latestChangedAtOf(toProcess, sinceEpochMs ?? undefined);
|
|
177
|
+
const localImportState = { source: flags.from };
|
|
178
|
+
if (latestLocalChangedAt !== undefined) {
|
|
179
|
+
localImportState.lastProcessedAt = latestLocalChangedAt;
|
|
180
|
+
}
|
|
181
|
+
this.log(`Fetched ${changes.length} change(s) from ${flags.from}; processing ${toProcess.length} into ${dir}.`);
|
|
182
|
+
const localResult = await applyLocalMigration({
|
|
183
|
+
changes: toProcess,
|
|
184
|
+
commitMessage: buildCommitMessage(flags.from, toProcess.length),
|
|
185
|
+
environments,
|
|
186
|
+
importState: localImportState,
|
|
187
|
+
localDir: dir,
|
|
188
|
+
reportData: buildReportData(flags.from, toProcess.length),
|
|
189
|
+
source,
|
|
190
|
+
});
|
|
191
|
+
this.log(localResult.committed
|
|
192
|
+
? `Committed ${toProcess.length} change(s). commit=${localResult.commitSha?.slice(0, 8) ?? ''} action=${localResult.action}`
|
|
193
|
+
: `No net changes produced by this run. Nothing to commit.`);
|
|
194
|
+
return {
|
|
195
|
+
...payload,
|
|
196
|
+
action: localResult.action,
|
|
197
|
+
commitSha: localResult.commitSha,
|
|
198
|
+
committed: localResult.committed,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error instanceof NotYetImplementedError)
|
|
203
|
+
return this.err(error.message);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async resolvePushWorkspace(slugOrId) {
|
|
208
|
+
const authConfig = await loadAuthConfig();
|
|
209
|
+
if (!authConfig)
|
|
210
|
+
return 'Not logged in. Run `qfg login` first, then re-run with --push.';
|
|
211
|
+
if (slugOrId) {
|
|
212
|
+
const resolved = resolveWorkspaceId(authConfig, slugOrId);
|
|
213
|
+
return { authConfig, workspaceId: resolved ?? slugOrId };
|
|
214
|
+
}
|
|
215
|
+
const activeProfile = getActiveProfile();
|
|
216
|
+
const profile = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
217
|
+
if (!profile) {
|
|
218
|
+
return 'No active profile found. Pass --workspace <slug> or run `qfg login` first.';
|
|
219
|
+
}
|
|
220
|
+
return { authConfig, workspaceId: profile.workspace };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function latestChangedAtOf(changes, fallback) {
|
|
224
|
+
let latest;
|
|
225
|
+
for (const change of changes) {
|
|
226
|
+
if (typeof change.changedAt === 'number' && (latest === undefined || change.changedAt > latest)) {
|
|
227
|
+
latest = change.changedAt;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return latest ?? fallback;
|
|
231
|
+
}
|
|
232
|
+
function buildCommitMessage(source, count) {
|
|
233
|
+
return `migrator: import ${count} change(s) from ${source}`;
|
|
234
|
+
}
|
|
235
|
+
function buildReportData(source, count) {
|
|
236
|
+
return {
|
|
237
|
+
cleanMappings: [],
|
|
238
|
+
counts: {
|
|
239
|
+
environmentsMapped: 0,
|
|
240
|
+
flagsMigrated: count,
|
|
241
|
+
itemsSkipped: 0,
|
|
242
|
+
segmentsMigrated: 0,
|
|
243
|
+
},
|
|
244
|
+
dryRun: false,
|
|
245
|
+
environmentMap: [],
|
|
246
|
+
followUp: { mustFixBeforeCutover: [], reviewPostCutover: [] },
|
|
247
|
+
identifierMap: {},
|
|
248
|
+
lossyMappings: [],
|
|
249
|
+
source,
|
|
250
|
+
unsupportedFeatures: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function resolveTargetDir(dirFlag, cwd) {
|
|
254
|
+
if (dirFlag)
|
|
255
|
+
return path.resolve(cwd, dirFlag);
|
|
256
|
+
if (fs.existsSync(path.join(cwd, 'quonfig.json')))
|
|
257
|
+
return cwd;
|
|
258
|
+
return path.resolve(cwd, DEFAULT_DIR);
|
|
259
|
+
}
|
|
260
|
+
function computeSince(flags, existing) {
|
|
261
|
+
if (flags.reset)
|
|
262
|
+
return null;
|
|
263
|
+
if (flags.since) {
|
|
264
|
+
const asNumber = Number(flags.since);
|
|
265
|
+
if (Number.isFinite(asNumber) && flags.since.trim() !== '')
|
|
266
|
+
return asNumber;
|
|
267
|
+
const asDate = new Date(flags.since).getTime();
|
|
268
|
+
return Number.isFinite(asDate) ? asDate : null;
|
|
269
|
+
}
|
|
270
|
+
const value = existing?.lastProcessedAt;
|
|
271
|
+
if (value === undefined)
|
|
272
|
+
return null;
|
|
273
|
+
if (typeof value === 'number')
|
|
274
|
+
return value;
|
|
275
|
+
const asDate = new Date(value).getTime();
|
|
276
|
+
return Number.isFinite(asDate) ? asDate : null;
|
|
277
|
+
}
|
|
@@ -8,7 +8,12 @@ import isInteractive from '../util/is-interactive.js';
|
|
|
8
8
|
import nameArg from '../util/name-arg.js';
|
|
9
9
|
export default class Override extends APICommand {
|
|
10
10
|
static args = { ...nameArg };
|
|
11
|
-
static description =
|
|
11
|
+
static description = `Override the value of an item for your user/SDK key combo.
|
|
12
|
+
|
|
13
|
+
To target users other than yourself, or to express rules like "user.email == X → value Y",
|
|
14
|
+
you cannot use override — see:
|
|
15
|
+
qfg config-schema # operator reference + worked multi-rule example
|
|
16
|
+
qfg pull # clone the workspace so you can edit JSON directly`;
|
|
12
17
|
static examples = [
|
|
13
18
|
'<%= config.bin %> <%= command.id %> # will prompt for name and value',
|
|
14
19
|
'<%= config.bin %> <%= command.id %> my.flag.name --value=true',
|
|
@@ -99,7 +104,6 @@ export default class Override extends APICommand {
|
|
|
99
104
|
async setOverride(config, value, environmentId) {
|
|
100
105
|
const { key, valueType, version } = config;
|
|
101
106
|
// Map the valueType to the format expected by the API
|
|
102
|
-
/* eslint-disable camelcase */
|
|
103
107
|
const typeMapping = {
|
|
104
108
|
bool: 'bool',
|
|
105
109
|
string: 'string',
|
|
@@ -111,7 +115,6 @@ export default class Override extends APICommand {
|
|
|
111
115
|
duration: 'duration',
|
|
112
116
|
int_range: 'intRange',
|
|
113
117
|
};
|
|
114
|
-
/* eslint-enable camelcase */
|
|
115
118
|
const type = typeMapping[valueType.toLowerCase()] || valueType;
|
|
116
119
|
// Parse the value based on type and build variant object
|
|
117
120
|
let variantValue = value;
|
|
@@ -2,7 +2,7 @@ import type { JsonObj } from '../result.js';
|
|
|
2
2
|
import { BaseCommand } from '../index.js';
|
|
3
3
|
export default class Profile extends BaseCommand {
|
|
4
4
|
static description: string;
|
|
5
|
-
static hidden: boolean;
|
|
6
5
|
static examples: string[];
|
|
6
|
+
static hidden: boolean;
|
|
7
7
|
run(): Promise<JsonObj | void>;
|
|
8
8
|
}
|
package/dist/commands/profile.js
CHANGED
|
@@ -2,8 +2,8 @@ import { BaseCommand } from '../index.js';
|
|
|
2
2
|
import WorkspaceSwitch from './workspace/switch.js';
|
|
3
3
|
export default class Profile extends BaseCommand {
|
|
4
4
|
static description = 'Deprecated. Use `qfg workspace switch` instead.';
|
|
5
|
-
static hidden = true;
|
|
6
5
|
static examples = ['<%= config.bin %> workspace switch'];
|
|
6
|
+
static hidden = true;
|
|
7
7
|
async run() {
|
|
8
8
|
this.log('Note: `qfg profile` is deprecated. Use `qfg workspace switch` instead.\n');
|
|
9
9
|
return WorkspaceSwitch.run([], this.config);
|
package/dist/commands/pull.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export default class Pull extends BaseCommand {
|
|
|
8
8
|
workspace: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
9
|
};
|
|
10
10
|
run(): Promise<JsonObj | void>;
|
|
11
|
-
private resolveRepoUrl;
|
|
12
|
-
private refreshAndGetUrl;
|
|
13
11
|
private looksLike401;
|
|
14
12
|
private maybeWriteQuonfigDir;
|
|
13
|
+
private refreshAndGetUrl;
|
|
14
|
+
private resolveRepoUrl;
|
|
15
15
|
}
|
package/dist/commands/pull.js
CHANGED
|
@@ -23,10 +23,7 @@ After editing files:
|
|
|
23
23
|
CLI shortcuts (no JSON editing needed for simple cases):
|
|
24
24
|
qfg set-rollout my.flag --environment production --true-percent 20
|
|
25
25
|
qfg set-default my.flag --environment production --value true`;
|
|
26
|
-
static examples = [
|
|
27
|
-
'<%= config.bin %> pull --dir ./our-config',
|
|
28
|
-
'<%= config.bin %> pull # uses QUONFIG_DIR env var',
|
|
29
|
-
];
|
|
26
|
+
static examples = ['<%= config.bin %> pull --dir ./our-config', '<%= config.bin %> pull # uses QUONFIG_DIR env var'];
|
|
30
27
|
static flags = {
|
|
31
28
|
dir: Flags.string({
|
|
32
29
|
description: 'Local directory to clone/update (defaults to QUONFIG_DIR env var)',
|
|
@@ -62,26 +59,7 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
62
59
|
const repoUrl = await this.resolveRepoUrl(workspaceId);
|
|
63
60
|
// Perform git operation
|
|
64
61
|
const isRepo = await isGitRepo(resolvedDir);
|
|
65
|
-
if (
|
|
66
|
-
// Clone fresh
|
|
67
|
-
this.log(`Cloning workspace into ${resolvedDir}...`);
|
|
68
|
-
try {
|
|
69
|
-
await gitClone(repoUrl, resolvedDir);
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
// On 401, refresh token and retry once
|
|
73
|
-
if (this.looksLike401(err)) {
|
|
74
|
-
this.verboseLog('Pull', 'Got 401, refreshing Gitea token...');
|
|
75
|
-
const freshUrl = await this.refreshAndGetUrl(workspaceId);
|
|
76
|
-
await gitClone(freshUrl, resolvedDir);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
throw err;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
this.log(`Cloned successfully into ${resolvedDir}`);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
62
|
+
if (isRepo) {
|
|
85
63
|
// Existing repo — check remote
|
|
86
64
|
const existingRemote = await getRemoteUrl(resolvedDir);
|
|
87
65
|
const expectedUrlBase = stripAuth(repoUrl);
|
|
@@ -100,15 +78,15 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
100
78
|
try {
|
|
101
79
|
await gitFetch(resolvedDir);
|
|
102
80
|
}
|
|
103
|
-
catch (
|
|
104
|
-
if (this.looksLike401(
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (this.looksLike401(error)) {
|
|
105
83
|
this.verboseLog('Pull', 'Got 401 on fetch, refreshing Gitea token...');
|
|
106
84
|
const freshUrl = await this.refreshAndGetUrl(workspaceId);
|
|
107
85
|
await gitSetRemote(resolvedDir, freshUrl);
|
|
108
86
|
await gitFetch(resolvedDir);
|
|
109
87
|
}
|
|
110
88
|
else {
|
|
111
|
-
throw
|
|
89
|
+
throw error;
|
|
112
90
|
}
|
|
113
91
|
}
|
|
114
92
|
const ff = await canFastForward(resolvedDir);
|
|
@@ -132,24 +110,29 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
132
110
|
this.log('Updated successfully.');
|
|
133
111
|
}
|
|
134
112
|
}
|
|
113
|
+
else {
|
|
114
|
+
// Clone fresh
|
|
115
|
+
this.log(`Cloning workspace into ${resolvedDir}...`);
|
|
116
|
+
try {
|
|
117
|
+
await gitClone(repoUrl, resolvedDir);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// On 401, refresh token and retry once
|
|
121
|
+
if (this.looksLike401(error)) {
|
|
122
|
+
this.verboseLog('Pull', 'Got 401, refreshing Gitea token...');
|
|
123
|
+
const freshUrl = await this.refreshAndGetUrl(workspaceId);
|
|
124
|
+
await gitClone(freshUrl, resolvedDir);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
this.log(`Cloned successfully into ${resolvedDir}`);
|
|
131
|
+
}
|
|
135
132
|
// Write QUONFIG_DIR to ~/.quonfig/config if not set
|
|
136
133
|
await this.maybeWriteQuonfigDir(resolvedDir);
|
|
137
134
|
return { dir: resolvedDir, workspaceId };
|
|
138
135
|
}
|
|
139
|
-
async resolveRepoUrl(workspaceId) {
|
|
140
|
-
let entry = await loadGiteaToken(workspaceId);
|
|
141
|
-
if (!entry || isGiteaTokenExpired(entry)) {
|
|
142
|
-
this.verboseLog('Pull', 'Minting new Gitea read token...');
|
|
143
|
-
entry = await mintAndStoreGiteaReadToken(workspaceId);
|
|
144
|
-
}
|
|
145
|
-
return entry.repoUrl;
|
|
146
|
-
}
|
|
147
|
-
async refreshAndGetUrl(workspaceId) {
|
|
148
|
-
const data = await mintGiteaToken(workspaceId, 'read', 'pull');
|
|
149
|
-
const entry = { token: data.token, repoUrl: data.repoUrl, expiresAt: data.expiresAt };
|
|
150
|
-
await saveGiteaToken(workspaceId, entry);
|
|
151
|
-
return entry.repoUrl;
|
|
152
|
-
}
|
|
153
136
|
looksLike401(err) {
|
|
154
137
|
const msg = err instanceof Error ? err.message : String(err);
|
|
155
138
|
return msg.includes('401') || msg.toLowerCase().includes('authentication failed');
|
|
@@ -172,6 +155,20 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
172
155
|
// Non-fatal
|
|
173
156
|
}
|
|
174
157
|
}
|
|
158
|
+
async refreshAndGetUrl(workspaceId) {
|
|
159
|
+
const data = await mintGiteaToken(workspaceId, 'read', 'pull');
|
|
160
|
+
const entry = { token: data.token, repoUrl: data.repoUrl, expiresAt: data.expiresAt };
|
|
161
|
+
await saveGiteaToken(workspaceId, entry);
|
|
162
|
+
return entry.repoUrl;
|
|
163
|
+
}
|
|
164
|
+
async resolveRepoUrl(workspaceId) {
|
|
165
|
+
let entry = await loadGiteaToken(workspaceId);
|
|
166
|
+
if (!entry || isGiteaTokenExpired(entry)) {
|
|
167
|
+
this.verboseLog('Pull', 'Minting new Gitea read token...');
|
|
168
|
+
entry = await mintAndStoreGiteaReadToken(workspaceId);
|
|
169
|
+
}
|
|
170
|
+
return entry.repoUrl;
|
|
171
|
+
}
|
|
175
172
|
}
|
|
176
173
|
/**
|
|
177
174
|
* Strip auth credentials from a git URL for comparison purposes.
|
|
@@ -13,9 +13,9 @@ export default class Schema extends APICommand {
|
|
|
13
13
|
'set-zod': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
14
14
|
};
|
|
15
15
|
run(): Promise<JsonObj | void>;
|
|
16
|
-
private getSchema;
|
|
17
|
-
private upsertSchema;
|
|
18
|
-
private fetchSchema;
|
|
19
16
|
private createSchema;
|
|
17
|
+
private fetchSchema;
|
|
18
|
+
private getSchema;
|
|
20
19
|
private updateSchema;
|
|
20
|
+
private upsertSchema;
|
|
21
21
|
}
|
package/dist/commands/schema.js
CHANGED
|
@@ -56,47 +56,34 @@ export default class Schema extends APICommand {
|
|
|
56
56
|
}
|
|
57
57
|
return this.upsertSchema(schemaKey, schemaInput, flags.protected);
|
|
58
58
|
}
|
|
59
|
-
async
|
|
60
|
-
const request = await this.apiClient.post('/api/v1/schemas/
|
|
59
|
+
async createSchema(schemaKey, schema, protectedSchema) {
|
|
60
|
+
const request = await this.apiClient.post('/api/v1/schemas/create', {
|
|
61
61
|
workspaceId: this.workspaceId,
|
|
62
62
|
schemaKey,
|
|
63
|
+
protected: protectedSchema,
|
|
64
|
+
schema,
|
|
63
65
|
});
|
|
64
66
|
if (!request.ok) {
|
|
65
|
-
if (request.status ===
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
67
|
+
if (request.status === 409) {
|
|
68
|
+
const current = await this.fetchSchema(schemaKey);
|
|
69
|
+
if (current) {
|
|
70
|
+
return this.updateSchema(schemaKey, schema, current, protectedSchema);
|
|
71
|
+
}
|
|
71
72
|
}
|
|
72
|
-
return this.err(`Failed to
|
|
73
|
+
return this.err(`Failed to create schema: ${schemaKey} | ${request.status} | ${JSON.stringify(request.error)}`, {
|
|
73
74
|
key: schemaKey,
|
|
74
|
-
phase: '
|
|
75
|
+
phase: 'creation',
|
|
75
76
|
serverError: request.error,
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
const response = request.json;
|
|
79
|
-
|
|
80
|
-
return this.err(`Schema ${schemaKey} returned an invalid document`, {
|
|
81
|
-
key: schemaKey,
|
|
82
|
-
phase: 'lookup',
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
return this.ok(JSON.stringify(response.schema, null, 2), {
|
|
80
|
+
return this.ok(`${checkmark} Created schema: ${schemaKey}`, {
|
|
86
81
|
commitSha: response.commitSha,
|
|
87
82
|
key: response.key,
|
|
88
83
|
protected: response.protected,
|
|
89
84
|
schema: response.schema,
|
|
90
85
|
});
|
|
91
86
|
}
|
|
92
|
-
async upsertSchema(schemaKey, schemaInput, protectedSchema) {
|
|
93
|
-
const schema = loadSchemaDocument(schemaInput);
|
|
94
|
-
const existing = await this.fetchSchema(schemaKey);
|
|
95
|
-
if (!existing) {
|
|
96
|
-
return this.createSchema(schemaKey, schema, protectedSchema);
|
|
97
|
-
}
|
|
98
|
-
return this.updateSchema(schemaKey, schema, existing, protectedSchema);
|
|
99
|
-
}
|
|
100
87
|
async fetchSchema(schemaKey) {
|
|
101
88
|
const request = await this.apiClient.post('/api/v1/schemas/getByKey', {
|
|
102
89
|
workspaceId: this.workspaceId,
|
|
@@ -114,28 +101,33 @@ export default class Schema extends APICommand {
|
|
|
114
101
|
}
|
|
115
102
|
throw new Error(`Failed to fetch schema ${schemaKey}: ${request.status} | ${JSON.stringify(request.error)}`);
|
|
116
103
|
}
|
|
117
|
-
async
|
|
118
|
-
const request = await this.apiClient.post('/api/v1/schemas/
|
|
104
|
+
async getSchema(schemaKey) {
|
|
105
|
+
const request = await this.apiClient.post('/api/v1/schemas/getByKey', {
|
|
119
106
|
workspaceId: this.workspaceId,
|
|
120
107
|
schemaKey,
|
|
121
|
-
protected: protectedSchema,
|
|
122
|
-
schema,
|
|
123
108
|
});
|
|
124
109
|
if (!request.ok) {
|
|
125
|
-
if (request.status ===
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
110
|
+
if (request.status === 404) {
|
|
111
|
+
return this.err(`Schema ${schemaKey} not found`, {
|
|
112
|
+
key: schemaKey,
|
|
113
|
+
phase: 'lookup',
|
|
114
|
+
serverError: request.error,
|
|
115
|
+
});
|
|
130
116
|
}
|
|
131
|
-
return this.err(`Failed to
|
|
117
|
+
return this.err(`Failed to fetch schema ${schemaKey}: ${request.status} | ${JSON.stringify(request.error)}`, {
|
|
132
118
|
key: schemaKey,
|
|
133
|
-
phase: '
|
|
119
|
+
phase: 'lookup',
|
|
134
120
|
serverError: request.error,
|
|
135
121
|
});
|
|
136
122
|
}
|
|
137
123
|
const response = request.json;
|
|
138
|
-
|
|
124
|
+
if (!isSchemaDocument(response.schema)) {
|
|
125
|
+
return this.err(`Schema ${schemaKey} returned an invalid document`, {
|
|
126
|
+
key: schemaKey,
|
|
127
|
+
phase: 'lookup',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return this.ok(JSON.stringify(response.schema, null, 2), {
|
|
139
131
|
commitSha: response.commitSha,
|
|
140
132
|
key: response.key,
|
|
141
133
|
protected: response.protected,
|
|
@@ -165,4 +157,12 @@ export default class Schema extends APICommand {
|
|
|
165
157
|
schema: response.schema,
|
|
166
158
|
});
|
|
167
159
|
}
|
|
160
|
+
async upsertSchema(schemaKey, schemaInput, protectedSchema) {
|
|
161
|
+
const schema = loadSchemaDocument(schemaInput);
|
|
162
|
+
const existing = await this.fetchSchema(schemaKey);
|
|
163
|
+
if (!existing) {
|
|
164
|
+
return this.createSchema(schemaKey, schema, protectedSchema);
|
|
165
|
+
}
|
|
166
|
+
return this.updateSchema(schemaKey, schema, existing, protectedSchema);
|
|
167
|
+
}
|
|
168
168
|
}
|
|
@@ -40,21 +40,19 @@ export default class SdkKeyCreate extends APICommand {
|
|
|
40
40
|
environmentId = match.id;
|
|
41
41
|
environmentName = match.name;
|
|
42
42
|
}
|
|
43
|
+
else if (environments.length === 1) {
|
|
44
|
+
environmentId = environments[0].id;
|
|
45
|
+
environmentName = environments[0].name;
|
|
46
|
+
this.log(`Using environment: ${environmentName}`);
|
|
47
|
+
}
|
|
43
48
|
else {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
choices: environments.map((e) => ({ name: e.name, value: e.id })),
|
|
52
|
-
message: 'Select environment:',
|
|
53
|
-
});
|
|
54
|
-
const match = environments.find((e) => e.id === chosen);
|
|
55
|
-
environmentId = match.id;
|
|
56
|
-
environmentName = match.name;
|
|
57
|
-
}
|
|
49
|
+
const chosen = await select({
|
|
50
|
+
choices: environments.map((e) => ({ name: e.name, value: e.id })),
|
|
51
|
+
message: 'Select environment:',
|
|
52
|
+
});
|
|
53
|
+
const match = environments.find((e) => e.id === chosen);
|
|
54
|
+
environmentId = match.id;
|
|
55
|
+
environmentName = match.name;
|
|
58
56
|
}
|
|
59
57
|
// Resolve key type
|
|
60
58
|
let keyType;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { JsonObj } from '../../result.js';
|
|
2
2
|
import { APICommand } from '../../index.js';
|
|
3
3
|
export default class SdkKeyRevoke extends APICommand {
|
|
4
|
-
static description: string;
|
|
5
4
|
static args: {
|
|
6
5
|
keyId: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
7
6
|
};
|
|
7
|
+
static description: string;
|
|
8
8
|
static examples: string[];
|
|
9
9
|
run(): Promise<JsonObj | void>;
|
|
10
10
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { APICommand } from '../../index.js';
|
|
3
3
|
export default class SdkKeyRevoke extends APICommand {
|
|
4
|
-
static description = 'Revoke an SDK key by its ID';
|
|
5
4
|
static args = {
|
|
6
5
|
keyId: Args.string({
|
|
7
6
|
description: 'ID of the SDK key to revoke',
|
|
8
7
|
required: true,
|
|
9
8
|
}),
|
|
10
9
|
};
|
|
11
|
-
static
|
|
12
|
-
|
|
13
|
-
];
|
|
10
|
+
static description = 'Revoke an SDK key by its ID';
|
|
11
|
+
static examples = ['<%= config.bin %> <%= command.id %> a1b2c3d4-e5f6-7890-abcd-ef1234567890'];
|
|
14
12
|
async run() {
|
|
15
13
|
const { args } = await this.parse(SdkKeyRevoke);
|
|
16
14
|
const request = await this.apiClient.post('/api/v1/sdkKeys/delete', {
|