@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.
- package/README.md +91 -33
- 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.js +1 -1
- package/dist/commands/migrate/my-code.d.ts +11 -0
- package/dist/commands/migrate/my-code.js +80 -0
- package/dist/commands/migrate/status.js +3 -16
- package/dist/commands/migrate.js +29 -8
- 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 +45 -47
- 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.js +3 -1
- package/dist/migrate/local-write.d.ts +26 -0
- package/dist/migrate/local-write.js +85 -0
- package/dist/migrate/migration-report.js +1 -9
- package/dist/migrate/push-strategy.js +1 -7
- package/dist/migrate/push-to-cloud.js +1 -1
- package/dist/migrate/sources/flagsmith.js +10 -0
- package/dist/quonfig.d.ts +1 -1
- package/dist/quonfig.js +5 -9
- package/dist/ui/get-value.js +1 -1
- 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/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 +252 -246
- package/oclif.manifest.json +79 -3
- package/package.json +6 -6
package/dist/commands/login.js
CHANGED
|
@@ -17,6 +17,9 @@ export default class Login extends BaseCommand {
|
|
|
17
17
|
async run() {
|
|
18
18
|
const { flags } = await this.parse(Login);
|
|
19
19
|
const profileName = flags.profile || 'default';
|
|
20
|
+
if (process.env.QUONFIG_API_KEY && process.env.QUONFIG_API_KEY.length > 0) {
|
|
21
|
+
this.log('Note: QUONFIG_API_KEY is set — runtime will prefer it over the login session.');
|
|
22
|
+
}
|
|
20
23
|
// Step 1: Request device code from WorkOS
|
|
21
24
|
this.verboseLog('Requesting device authorization code...');
|
|
22
25
|
const deviceAuth = await requestDeviceCode();
|
|
@@ -65,13 +68,13 @@ export default class Login extends BaseCommand {
|
|
|
65
68
|
const res = await fetch(`${apiUrl}/api/v1/userWorkspaces/list`, {
|
|
66
69
|
method: 'POST',
|
|
67
70
|
headers: {
|
|
68
|
-
|
|
71
|
+
Authorization: `Bearer ${tokenResponse.access_token}`,
|
|
69
72
|
'Content-Type': 'application/json',
|
|
70
73
|
},
|
|
71
74
|
body: JSON.stringify({ json: {} }),
|
|
72
75
|
});
|
|
73
76
|
if (res.ok) {
|
|
74
|
-
const body = await res.json();
|
|
77
|
+
const body = (await res.json());
|
|
75
78
|
const allWorkspaces = (body.json ?? body);
|
|
76
79
|
const candidates = Array.isArray(allWorkspaces) ? allWorkspaces : [];
|
|
77
80
|
multipleWorkspacesAvailable = candidates.length > 1;
|
|
@@ -81,7 +84,10 @@ export default class Login extends BaseCommand {
|
|
|
81
84
|
}
|
|
82
85
|
else if (candidates.length > 1) {
|
|
83
86
|
const chosen = await select({
|
|
84
|
-
choices: candidates.map((w) => ({
|
|
87
|
+
choices: candidates.map((w) => ({
|
|
88
|
+
name: `${w.organizationName} / ${w.workspaceSlug}`,
|
|
89
|
+
value: w.workspaceId,
|
|
90
|
+
})),
|
|
85
91
|
message: 'Select workspace:',
|
|
86
92
|
});
|
|
87
93
|
match = candidates.find((w) => w.workspaceId === chosen);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../index.js';
|
|
3
|
-
import { formatHumanReport, runDoctor
|
|
3
|
+
import { formatHumanReport, runDoctor } from '../../migrate/doctor.js';
|
|
4
4
|
import { loadTokens } from '../../util/token-storage.js';
|
|
5
5
|
const SUPPORTED_SOURCES = new Set(['launch']);
|
|
6
6
|
const defaultLoadSession = async () => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { JsonObj } from '../../result.js';
|
|
2
|
+
import { BaseCommand } from '../../index.js';
|
|
3
|
+
export default class MigrateMyCode extends BaseCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
'dry-run': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
8
|
+
from: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<JsonObj | void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
const SKILL_NAME = 'qfg-migrate-code';
|
|
6
|
+
const SUPPORTED_SOURCES = new Set(['launch']);
|
|
7
|
+
function findIdentifierMap(startDir) {
|
|
8
|
+
let dir = path.resolve(startDir);
|
|
9
|
+
while (true) {
|
|
10
|
+
const candidate = path.join(dir, '.qf', 'identifier-map.json');
|
|
11
|
+
if (fs.existsSync(candidate)) {
|
|
12
|
+
const raw = fs.readFileSync(candidate, 'utf8');
|
|
13
|
+
return { dir, map: JSON.parse(raw) };
|
|
14
|
+
}
|
|
15
|
+
const parent = path.dirname(dir);
|
|
16
|
+
if (parent === dir)
|
|
17
|
+
return null;
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export default class MigrateMyCode extends BaseCommand {
|
|
22
|
+
static description = 'Migrate Launch SDK call sites in your codebase to Quonfig SDK call sites (invokes the qfg-migrate-code Claude skill)';
|
|
23
|
+
static examples = [
|
|
24
|
+
'<%= config.bin %> <%= command.id %>',
|
|
25
|
+
'<%= config.bin %> <%= command.id %> --from launch',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> --dry-run',
|
|
27
|
+
];
|
|
28
|
+
static flags = {
|
|
29
|
+
'dry-run': Flags.boolean({
|
|
30
|
+
default: false,
|
|
31
|
+
description: 'Show what would be migrated without editing any files',
|
|
32
|
+
}),
|
|
33
|
+
from: Flags.string({
|
|
34
|
+
default: 'launch',
|
|
35
|
+
description: 'Legacy SDK to migrate from',
|
|
36
|
+
options: ['launch'],
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
async run() {
|
|
40
|
+
const { flags } = await this.parse(MigrateMyCode);
|
|
41
|
+
if (!SUPPORTED_SOURCES.has(flags.from)) {
|
|
42
|
+
return this.err(`Unsupported --from source: ${flags.from}. Only 'launch' is supported today; 'launchdarkly' and 'flagsmith' will follow.`);
|
|
43
|
+
}
|
|
44
|
+
const found = findIdentifierMap(process.cwd());
|
|
45
|
+
if (!found) {
|
|
46
|
+
return this.err(`Could not find .qf/identifier-map.json in the current directory or any parent.\n\n` +
|
|
47
|
+
`This file is produced by \`qfg migrate --from ${flags.from}\` and maps legacy keys to their Quonfig equivalents.\n` +
|
|
48
|
+
`Run \`qfg migrate --from ${flags.from} --dir <your-workspace-dir>\` first, then re-run this command from your codebase.`);
|
|
49
|
+
}
|
|
50
|
+
const { dir: workspaceDir, map: mappings } = found;
|
|
51
|
+
const mappingCount = Object.keys(mappings).length;
|
|
52
|
+
const relativeMapPath = path.relative(process.cwd(), path.join(workspaceDir, '.qf', 'identifier-map.json')) || '.qf/identifier-map.json';
|
|
53
|
+
this.log(`Skill: ${SKILL_NAME}`);
|
|
54
|
+
this.log(`From: ${flags.from}`);
|
|
55
|
+
this.log(`Identifier map: ${relativeMapPath}`);
|
|
56
|
+
this.log(`Found: ${mappingCount} identifier remapping${mappingCount === 1 ? '' : 's'}`);
|
|
57
|
+
if (flags['dry-run']) {
|
|
58
|
+
this.log(`Mode: dry-run (no files will be modified)`);
|
|
59
|
+
}
|
|
60
|
+
this.log('');
|
|
61
|
+
this.log(`To run the migration, invoke Claude with the ${SKILL_NAME} skill. For example:`);
|
|
62
|
+
this.log('');
|
|
63
|
+
this.log(` claude "/${SKILL_NAME}${flags['dry-run'] ? ' --dry-run' : ''}"`);
|
|
64
|
+
this.log('');
|
|
65
|
+
this.log(`The skill will:`);
|
|
66
|
+
this.log(` 1. Scan for ${flags.from} SDK imports and call sites`);
|
|
67
|
+
this.log(` 2. Propose edits using the ${mappingCount} key remapping${mappingCount === 1 ? '' : 's'} above`);
|
|
68
|
+
this.log(` 3. Ask for approval before editing files`);
|
|
69
|
+
this.log(` 4. Run your tests / typecheck afterwards if available`);
|
|
70
|
+
this.log(` 5. Summarize anything it could not auto-migrate`);
|
|
71
|
+
return {
|
|
72
|
+
dryRun: flags['dry-run'],
|
|
73
|
+
from: flags.from,
|
|
74
|
+
identifierMapPath: path.join(workspaceDir, '.qf', 'identifier-map.json'),
|
|
75
|
+
mappingCount,
|
|
76
|
+
mappings,
|
|
77
|
+
skill: SKILL_NAME,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -10,10 +10,7 @@ const NO_STATE_MESSAGE = 'No migration state found. Run qfg migrate --from <sour
|
|
|
10
10
|
function countJsonFiles(dir) {
|
|
11
11
|
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
|
12
12
|
return 0;
|
|
13
|
-
return fs
|
|
14
|
-
.readdirSync(dir)
|
|
15
|
-
.filter((name) => name.endsWith('.json') && !name.startsWith('.'))
|
|
16
|
-
.length;
|
|
13
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith('.json') && !name.startsWith('.')).length;
|
|
17
14
|
}
|
|
18
15
|
function countEnvironments(dir) {
|
|
19
16
|
const file = path.join(dir, WORKSPACE_FILE);
|
|
@@ -35,7 +32,7 @@ function workspaceCounts(dir) {
|
|
|
35
32
|
};
|
|
36
33
|
}
|
|
37
34
|
function toIsoTimestamp(value) {
|
|
38
|
-
if (value
|
|
35
|
+
if (value === null || value === undefined)
|
|
39
36
|
return null;
|
|
40
37
|
const date = typeof value === 'number' ? new Date(value) : new Date(value);
|
|
41
38
|
if (Number.isNaN(date.getTime()))
|
|
@@ -50,17 +47,7 @@ function nextHint(source, counts) {
|
|
|
50
47
|
}
|
|
51
48
|
function formatHuman(payload) {
|
|
52
49
|
const lines = [];
|
|
53
|
-
lines.push(`Source: ${payload.source}`);
|
|
54
|
-
lines.push(`Source workspace: ${payload.sourceWorkspaceId ?? '(not recorded)'}`);
|
|
55
|
-
lines.push(`Last processed at: ${payload.lastProcessedAtIso ?? '(not recorded)'}`);
|
|
56
|
-
lines.push(`Workspace dir: ${payload.dir}`);
|
|
57
|
-
lines.push('');
|
|
58
|
-
lines.push('Counts:');
|
|
59
|
-
lines.push(` Flags: ${payload.counts.flags}`);
|
|
60
|
-
lines.push(` Segments: ${payload.counts.segments}`);
|
|
61
|
-
lines.push(` Environments: ${payload.counts.environments}`);
|
|
62
|
-
lines.push('');
|
|
63
|
-
lines.push(`Next: ${payload.next}`);
|
|
50
|
+
lines.push(`Source: ${payload.source}`, `Source workspace: ${payload.sourceWorkspaceId ?? '(not recorded)'}`, `Last processed at: ${payload.lastProcessedAtIso ?? '(not recorded)'}`, `Workspace dir: ${payload.dir}`, '', 'Counts:', ` Flags: ${payload.counts.flags}`, ` Segments: ${payload.counts.segments}`, ` Environments: ${payload.counts.environments}`, '', `Next: ${payload.next}`);
|
|
64
51
|
return lines.join('\n');
|
|
65
52
|
}
|
|
66
53
|
export default class MigrateStatus extends BaseCommand {
|
package/dist/commands/migrate.js
CHANGED
|
@@ -2,7 +2,8 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { BaseCommand } from '../index.js';
|
|
5
|
-
import { CrossSourceError, assertSourceMatches, readImportState
|
|
5
|
+
import { CrossSourceError, assertSourceMatches, readImportState } from '../migrate/import-state.js';
|
|
6
|
+
import { applyLocalMigration } from '../migrate/local-write.js';
|
|
6
7
|
import { PushConflictError } from '../migrate/push-strategy.js';
|
|
7
8
|
import { pushMigrationToCloud } from '../migrate/push-to-cloud.js';
|
|
8
9
|
import { UnknownSourceError, getSource } from '../migrate/registry.js';
|
|
@@ -10,7 +11,7 @@ import { NotYetImplementedError } from '../migrate/source.js';
|
|
|
10
11
|
import { applyLaunchBaseUrl } from '../migrate/sources/launch/api.js';
|
|
11
12
|
import { mintGiteaToken } from '../util/gitea-api.js';
|
|
12
13
|
import { displayUrl } from '../util/git-ops.js';
|
|
13
|
-
import { getActiveProfile, loadAuthConfig, resolveWorkspaceId
|
|
14
|
+
import { getActiveProfile, loadAuthConfig, resolveWorkspaceId } from '../util/token-storage.js';
|
|
14
15
|
const DEFAULT_DIR = 'quonfig-config';
|
|
15
16
|
export default class Migrate extends BaseCommand {
|
|
16
17
|
static description = 'Migrate flags and configs from a legacy feature-flag system into a Quonfig workspace.\n\n' +
|
|
@@ -105,7 +106,7 @@ export default class Migrate extends BaseCommand {
|
|
|
105
106
|
}
|
|
106
107
|
try {
|
|
107
108
|
await source.validateAuth(flags['api-key']);
|
|
108
|
-
await source.listEnvironments();
|
|
109
|
+
const environments = await source.listEnvironments();
|
|
109
110
|
const existing = readImportState(dir);
|
|
110
111
|
const sinceEpochMs = computeSince(flags, existing);
|
|
111
112
|
const changes = [];
|
|
@@ -172,9 +173,30 @@ export default class Migrate extends BaseCommand {
|
|
|
172
173
|
throw error;
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
};
|
|
178
200
|
}
|
|
179
201
|
catch (error) {
|
|
180
202
|
if (error instanceof NotYetImplementedError)
|
|
@@ -191,8 +213,7 @@ export default class Migrate extends BaseCommand {
|
|
|
191
213
|
return { authConfig, workspaceId: resolved ?? slugOrId };
|
|
192
214
|
}
|
|
193
215
|
const activeProfile = getActiveProfile();
|
|
194
|
-
const profile = authConfig.profiles[activeProfile] ||
|
|
195
|
-
authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
216
|
+
const profile = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
196
217
|
if (!profile) {
|
|
197
218
|
return 'No active profile found. Pass --workspace <slug> or run `qfg login` first.';
|
|
198
219
|
}
|
|
@@ -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
|
@@ -5,7 +5,7 @@ import { BaseCommand } from '../index.js';
|
|
|
5
5
|
import { getActiveProfile, loadAuthConfig } from '../util/token-storage.js';
|
|
6
6
|
import { loadGiteaToken, isGiteaTokenExpired, saveGiteaToken } from '../util/gitea-token-storage.js';
|
|
7
7
|
import { mintAndStoreGiteaReadToken, mintGiteaToken } from '../util/gitea-api.js';
|
|
8
|
-
import { isGitRepo, getRemoteUrl,
|
|
8
|
+
import { isGitRepo, getRemoteUrl, gitFetch, gitSetRemote, canFastForward, hasDivergedFromRemote, gitMergeFfOnly, gitClone, displayUrl, } from '../util/git-ops.js';
|
|
9
9
|
export default class Pull extends BaseCommand {
|
|
10
10
|
static description = `Clone or update a local copy of your workspace config files.
|
|
11
11
|
|
|
@@ -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);
|
|
@@ -90,25 +68,26 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
90
68
|
}
|
|
91
69
|
// Update the remote URL to use current (possibly refreshed) token
|
|
92
70
|
await gitSetRemote(resolvedDir, repoUrl);
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
71
|
+
// Note: we intentionally don't pre-check for untracked or modified files.
|
|
72
|
+
// Plain `git pull` is fine with untracked files, and `git merge --ff-only`
|
|
73
|
+
// below will fail loudly on real conflicts (tracked modifications, or an
|
|
74
|
+
// incoming commit that would overwrite an untracked file). Pre-checking
|
|
75
|
+
// just creates a self-trap when qfg itself writes files like
|
|
76
|
+
// `.qfg-friction.log` into the workspace dir (qfg-q2l).
|
|
98
77
|
// Fetch
|
|
99
78
|
this.log('Fetching from remote...');
|
|
100
79
|
try {
|
|
101
80
|
await gitFetch(resolvedDir);
|
|
102
81
|
}
|
|
103
|
-
catch (
|
|
104
|
-
if (this.looksLike401(
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (this.looksLike401(error)) {
|
|
105
84
|
this.verboseLog('Pull', 'Got 401 on fetch, refreshing Gitea token...');
|
|
106
85
|
const freshUrl = await this.refreshAndGetUrl(workspaceId);
|
|
107
86
|
await gitSetRemote(resolvedDir, freshUrl);
|
|
108
87
|
await gitFetch(resolvedDir);
|
|
109
88
|
}
|
|
110
89
|
else {
|
|
111
|
-
throw
|
|
90
|
+
throw error;
|
|
112
91
|
}
|
|
113
92
|
}
|
|
114
93
|
const ff = await canFastForward(resolvedDir);
|
|
@@ -132,24 +111,29 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
132
111
|
this.log('Updated successfully.');
|
|
133
112
|
}
|
|
134
113
|
}
|
|
114
|
+
else {
|
|
115
|
+
// Clone fresh
|
|
116
|
+
this.log(`Cloning workspace into ${resolvedDir}...`);
|
|
117
|
+
try {
|
|
118
|
+
await gitClone(repoUrl, resolvedDir);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// On 401, refresh token and retry once
|
|
122
|
+
if (this.looksLike401(error)) {
|
|
123
|
+
this.verboseLog('Pull', 'Got 401, refreshing Gitea token...');
|
|
124
|
+
const freshUrl = await this.refreshAndGetUrl(workspaceId);
|
|
125
|
+
await gitClone(freshUrl, resolvedDir);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.log(`Cloned successfully into ${resolvedDir}`);
|
|
132
|
+
}
|
|
135
133
|
// Write QUONFIG_DIR to ~/.quonfig/config if not set
|
|
136
134
|
await this.maybeWriteQuonfigDir(resolvedDir);
|
|
137
135
|
return { dir: resolvedDir, workspaceId };
|
|
138
136
|
}
|
|
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
137
|
looksLike401(err) {
|
|
154
138
|
const msg = err instanceof Error ? err.message : String(err);
|
|
155
139
|
return msg.includes('401') || msg.toLowerCase().includes('authentication failed');
|
|
@@ -172,6 +156,20 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
172
156
|
// Non-fatal
|
|
173
157
|
}
|
|
174
158
|
}
|
|
159
|
+
async refreshAndGetUrl(workspaceId) {
|
|
160
|
+
const data = await mintGiteaToken(workspaceId, 'read', 'pull');
|
|
161
|
+
const entry = { token: data.token, repoUrl: data.repoUrl, expiresAt: data.expiresAt };
|
|
162
|
+
await saveGiteaToken(workspaceId, entry);
|
|
163
|
+
return entry.repoUrl;
|
|
164
|
+
}
|
|
165
|
+
async resolveRepoUrl(workspaceId) {
|
|
166
|
+
let entry = await loadGiteaToken(workspaceId);
|
|
167
|
+
if (!entry || isGiteaTokenExpired(entry)) {
|
|
168
|
+
this.verboseLog('Pull', 'Minting new Gitea read token...');
|
|
169
|
+
entry = await mintAndStoreGiteaReadToken(workspaceId);
|
|
170
|
+
}
|
|
171
|
+
return entry.repoUrl;
|
|
172
|
+
}
|
|
175
173
|
}
|
|
176
174
|
/**
|
|
177
175
|
* 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
|
}
|