@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
package/dist/commands/info.js
CHANGED
|
@@ -195,7 +195,6 @@ Related commands:
|
|
|
195
195
|
// Fetch stats for all environments (similar to original implementation)
|
|
196
196
|
const endTime = Date.now();
|
|
197
197
|
const startTime = endTime - 24 * 60 * 60 * 1000; // 24 hours ago
|
|
198
|
-
const timeInterval = 'HOURLY';
|
|
199
198
|
const statsPerEnv = {};
|
|
200
199
|
// Fetch stats for each environment via oRPC
|
|
201
200
|
for (const env of environments) {
|
|
@@ -275,6 +274,10 @@ Related commands:
|
|
|
275
274
|
if (value.type === 'stringList' && Array.isArray(value.value)) {
|
|
276
275
|
return value.value.join(',');
|
|
277
276
|
}
|
|
277
|
+
// JSON values must be stringified, else they render as [object Object]
|
|
278
|
+
if (value.type === 'json') {
|
|
279
|
+
return JSON.stringify(value.value);
|
|
280
|
+
}
|
|
278
281
|
return String(value.value);
|
|
279
282
|
}
|
|
280
283
|
// New format: {string: 'abc'}
|
package/dist/commands/init.js
CHANGED
|
@@ -55,7 +55,12 @@ export default class Init extends BaseCommand {
|
|
|
55
55
|
this.log(' Nothing to do.');
|
|
56
56
|
}
|
|
57
57
|
if (this.jsonEnabled()) {
|
|
58
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
actions: plan.actions,
|
|
60
|
+
dryRun: true,
|
|
61
|
+
isFirstTime: plan.isFirstTime,
|
|
62
|
+
samplesIncluded: plan.samplesIncluded,
|
|
63
|
+
};
|
|
59
64
|
}
|
|
60
65
|
return plan;
|
|
61
66
|
}
|
|
@@ -76,7 +81,12 @@ export default class Init extends BaseCommand {
|
|
|
76
81
|
}
|
|
77
82
|
this.log('\nRun `qfg verify` to validate the workspace.');
|
|
78
83
|
if (this.jsonEnabled()) {
|
|
79
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
actions: plan.actions,
|
|
86
|
+
dryRun: false,
|
|
87
|
+
isFirstTime: plan.isFirstTime,
|
|
88
|
+
samplesIncluded: plan.samplesIncluded,
|
|
89
|
+
};
|
|
80
90
|
}
|
|
81
91
|
return plan;
|
|
82
92
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { BaseCommand } from '../index.js';
|
|
2
2
|
import { interactivePrompt } from '../interactive-prompt.js';
|
|
3
3
|
export default class Interactive extends BaseCommand {
|
|
4
|
+
static description = `Launch an interactive menu to browse and manage your workspace.
|
|
5
|
+
|
|
6
|
+
Common shortcuts:
|
|
7
|
+
qfg set-default my.flag --environment production --value false # catch-all fallback
|
|
8
|
+
qfg set-rollout my.flag --environment production --true-percent 20 # % rollout
|
|
9
|
+
|
|
10
|
+
For arbitrary targeting rules (e.g. user.email, plan, segment, custom property),
|
|
11
|
+
run 'qfg config-schema' then 'qfg pull' and edit the JSON config directly.`;
|
|
4
12
|
static examples = ['<%= config.bin %>'];
|
|
5
13
|
async run() {
|
|
6
14
|
await interactivePrompt(this.config);
|
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);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../index.js';
|
|
2
|
+
export default class MigrateDoctor extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'api-key': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
dir: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
from: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
|
+
language: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../index.js';
|
|
3
|
+
import { formatHumanReport, runDoctor } from '../../migrate/doctor.js';
|
|
4
|
+
import { loadTokens } from '../../util/token-storage.js';
|
|
5
|
+
const SUPPORTED_SOURCES = new Set(['launch']);
|
|
6
|
+
const defaultLoadSession = async () => {
|
|
7
|
+
const tokens = await loadTokens();
|
|
8
|
+
if (!tokens?.accessToken)
|
|
9
|
+
return null;
|
|
10
|
+
return { expiresAt: tokens.expiresAt ?? 0 };
|
|
11
|
+
};
|
|
12
|
+
export default class MigrateDoctor extends BaseCommand {
|
|
13
|
+
static description = 'Preflight health checks for `qfg migrate`. Verifies auth, workspace provisioning, working tree, and identifier collisions.';
|
|
14
|
+
static examples = [
|
|
15
|
+
'<%= config.bin %> <%= command.id %>',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY',
|
|
17
|
+
'<%= config.bin %> <%= command.id %> --dir ./my-workspace --language node',
|
|
18
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
'api-key': Flags.string({
|
|
22
|
+
description: 'Legacy SDK API key to validate against the source',
|
|
23
|
+
required: false,
|
|
24
|
+
}),
|
|
25
|
+
dir: Flags.string({
|
|
26
|
+
default: '.',
|
|
27
|
+
description: 'Workspace directory to check (git clean / identifier map)',
|
|
28
|
+
}),
|
|
29
|
+
from: Flags.string({
|
|
30
|
+
default: 'launch',
|
|
31
|
+
description: 'Legacy SDK to migrate from',
|
|
32
|
+
options: ['launch'],
|
|
33
|
+
}),
|
|
34
|
+
language: Flags.string({
|
|
35
|
+
description: 'Customer SDK language — used to warn when datadir mode is unavailable (e.g. javascript-browser)',
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
async run() {
|
|
40
|
+
const { flags } = await this.parse(MigrateDoctor);
|
|
41
|
+
if (!SUPPORTED_SOURCES.has(flags.from)) {
|
|
42
|
+
this.err(`Unsupported --from source: ${flags.from}. Only 'launch' is supported today.`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const ctx = {
|
|
46
|
+
apiKey: flags['api-key'],
|
|
47
|
+
dir: flags.dir,
|
|
48
|
+
from: flags.from,
|
|
49
|
+
language: flags.language,
|
|
50
|
+
loadSession: defaultLoadSession,
|
|
51
|
+
};
|
|
52
|
+
const report = await runDoctor(ctx);
|
|
53
|
+
if (this.jsonEnabled()) {
|
|
54
|
+
process.stdout.write(JSON.stringify(report) + '\n');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.log(formatHumanReport(report));
|
|
58
|
+
}
|
|
59
|
+
if (!report.passed) {
|
|
60
|
+
this.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { JsonObj } from '../../result.js';
|
|
2
|
+
import { BaseCommand } from '../../index.js';
|
|
3
|
+
export default class MigrateStatus extends BaseCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
dir: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<JsonObj | void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { readImportState } from '../../migrate/import-state.js';
|
|
6
|
+
const FLAGS_DIR = 'feature-flags';
|
|
7
|
+
const SEGMENTS_DIR = 'segments';
|
|
8
|
+
const WORKSPACE_FILE = 'quonfig.json';
|
|
9
|
+
const NO_STATE_MESSAGE = 'No migration state found. Run qfg migrate --from <source> first.';
|
|
10
|
+
function countJsonFiles(dir) {
|
|
11
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
|
12
|
+
return 0;
|
|
13
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith('.json') && !name.startsWith('.')).length;
|
|
14
|
+
}
|
|
15
|
+
function countEnvironments(dir) {
|
|
16
|
+
const file = path.join(dir, WORKSPACE_FILE);
|
|
17
|
+
if (!fs.existsSync(file))
|
|
18
|
+
return 0;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
21
|
+
return Array.isArray(parsed.environments) ? parsed.environments.length : 0;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function workspaceCounts(dir) {
|
|
28
|
+
return {
|
|
29
|
+
environments: countEnvironments(dir),
|
|
30
|
+
flags: countJsonFiles(path.join(dir, FLAGS_DIR)),
|
|
31
|
+
segments: countJsonFiles(path.join(dir, SEGMENTS_DIR)),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function toIsoTimestamp(value) {
|
|
35
|
+
if (value === null || value === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
const date = typeof value === 'number' ? new Date(value) : new Date(value);
|
|
38
|
+
if (Number.isNaN(date.getTime()))
|
|
39
|
+
return null;
|
|
40
|
+
return date.toISOString();
|
|
41
|
+
}
|
|
42
|
+
function nextHint(source, counts) {
|
|
43
|
+
if (counts.flags === 0 && counts.segments === 0) {
|
|
44
|
+
return `Run \`qfg migrate --from ${source}\` to import flags and segments.`;
|
|
45
|
+
}
|
|
46
|
+
return `Run \`qfg migrate my-code --from ${source}\` to update your SDK call sites, or re-run \`qfg migrate --from ${source}\` to pick up changes.`;
|
|
47
|
+
}
|
|
48
|
+
function formatHuman(payload) {
|
|
49
|
+
const lines = [];
|
|
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}`);
|
|
51
|
+
return lines.join('\n');
|
|
52
|
+
}
|
|
53
|
+
export default class MigrateStatus extends BaseCommand {
|
|
54
|
+
static description = 'Show the status of a workspace migrated with `qfg migrate`. Reads .qf/import-state.json and summarizes source, counts, and next steps.';
|
|
55
|
+
static examples = [
|
|
56
|
+
'<%= config.bin %> <%= command.id %>',
|
|
57
|
+
'<%= config.bin %> <%= command.id %> --dir ./my-workspace',
|
|
58
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
59
|
+
];
|
|
60
|
+
static flags = {
|
|
61
|
+
dir: Flags.string({
|
|
62
|
+
default: '.',
|
|
63
|
+
description: 'Workspace directory to inspect',
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
async run() {
|
|
67
|
+
const { flags } = await this.parse(MigrateStatus);
|
|
68
|
+
const dir = path.resolve(flags.dir);
|
|
69
|
+
const state = readImportState(dir);
|
|
70
|
+
if (!state) {
|
|
71
|
+
const payload = {
|
|
72
|
+
dir,
|
|
73
|
+
found: false,
|
|
74
|
+
message: NO_STATE_MESSAGE,
|
|
75
|
+
};
|
|
76
|
+
if (this.jsonEnabled())
|
|
77
|
+
return payload;
|
|
78
|
+
this.log(NO_STATE_MESSAGE);
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
const counts = workspaceCounts(dir);
|
|
82
|
+
const lastProcessedAtIso = toIsoTimestamp(state.lastProcessedAt);
|
|
83
|
+
const next = nextHint(state.source, counts);
|
|
84
|
+
const payload = {
|
|
85
|
+
counts,
|
|
86
|
+
dir,
|
|
87
|
+
found: true,
|
|
88
|
+
lastProcessedAt: state.lastProcessedAt ?? null,
|
|
89
|
+
lastProcessedAtIso,
|
|
90
|
+
next,
|
|
91
|
+
source: state.source,
|
|
92
|
+
sourceWorkspaceId: state.sourceWorkspaceId ?? null,
|
|
93
|
+
};
|
|
94
|
+
if (this.jsonEnabled())
|
|
95
|
+
return payload;
|
|
96
|
+
this.log(formatHuman(payload));
|
|
97
|
+
return payload;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JsonObj } from '../result.js';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
export default class Migrate extends BaseCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
'api-key': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
dir: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
|
+
'dry-run': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
from: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
push: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
12
|
+
recent: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
reset: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
14
|
+
since: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
15
|
+
staging: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
16
|
+
workspace: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<JsonObj | void>;
|
|
19
|
+
private resolvePushWorkspace;
|
|
20
|
+
}
|