@quonfig/cli 0.0.37 → 0.0.39
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 +288 -35
- package/dist/codegen/local-config-reader.js +30 -5
- package/dist/codegen/mustache-extractor.js +1 -1
- package/dist/codegen/schema-extractor.js +4 -2
- package/dist/commands/activity/deleted.d.ts +7 -0
- package/dist/commands/activity/deleted.js +32 -0
- package/dist/commands/activity/feed.d.ts +10 -0
- package/dist/commands/activity/feed.js +77 -0
- package/dist/commands/activity/history.d.ts +10 -0
- package/dist/commands/activity/history.js +61 -0
- package/dist/commands/activity/restore.d.ts +21 -0
- package/dist/commands/activity/restore.js +113 -0
- package/dist/commands/activity.d.ts +6 -0
- package/dist/commands/activity.js +17 -0
- package/dist/commands/audit-log.d.ts +19 -0
- package/dist/commands/audit-log.js +30 -0
- package/dist/commands/config-schema.d.ts +1 -0
- package/dist/commands/config-schema.js +4 -1
- package/dist/commands/history.d.ts +14 -0
- package/dist/commands/history.js +19 -0
- package/dist/commands/info.js +1 -1
- package/dist/commands/log.d.ts +11 -0
- package/dist/commands/log.js +15 -0
- package/dist/init/schema.js +1 -24
- package/dist/quonfig.js +1 -1
- package/dist/util/activity-lookup.d.ts +25 -0
- package/dist/util/activity-lookup.js +36 -0
- package/dist/verify/validate.d.ts +1 -0
- package/dist/verify/validate.js +3 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +574 -1
- package/package.json +4 -3
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { APICommand } from '../../index.js';
|
|
2
|
+
export default class ActivityDeleted extends APICommand {
|
|
3
|
+
static description = `List configs that were deleted from the workspace and have not been restored.
|
|
4
|
+
|
|
5
|
+
Useful for "did somebody delete X?" — pair with \`qfg activity restore NAME\`
|
|
6
|
+
to undelete a tombstoned config.`;
|
|
7
|
+
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --json'];
|
|
8
|
+
async run() {
|
|
9
|
+
const request = await this.apiClient.post('/api/v1/activity/getDeletedItems', {
|
|
10
|
+
workspaceId: this.workspaceId,
|
|
11
|
+
});
|
|
12
|
+
if (!request.ok) {
|
|
13
|
+
const errorMsg = request.error?.error || `Failed to fetch deleted items: ${request.status}`;
|
|
14
|
+
return this.err(errorMsg, { serverError: request.error });
|
|
15
|
+
}
|
|
16
|
+
const items = (Array.isArray(request.json) ? request.json : []);
|
|
17
|
+
if (items.length === 0) {
|
|
18
|
+
this.log('No deleted items in this workspace.');
|
|
19
|
+
return { items: [] };
|
|
20
|
+
}
|
|
21
|
+
const maxTypeLen = Math.max(4, ...items.map((i) => i.configType.length));
|
|
22
|
+
const maxKeyLen = Math.max(3, ...items.map((i) => i.configKey.length));
|
|
23
|
+
const maxByLen = Math.max(2, ...items.map((i) => i.deletedBy.length));
|
|
24
|
+
const header = `${'TYPE'.padEnd(maxTypeLen)} ${'KEY'.padEnd(maxKeyLen)} ${'BY'.padEnd(maxByLen)} DELETED AT`;
|
|
25
|
+
const sep = `${'-'.repeat(maxTypeLen)} ${'-'.repeat(maxKeyLen)} ${'-'.repeat(maxByLen)} ${'-'.repeat(20)}`;
|
|
26
|
+
const rows = items.map((i) => `${i.configType.padEnd(maxTypeLen)} ${i.configKey.padEnd(maxKeyLen)} ${i.deletedBy.padEnd(maxByLen)} ${i.deletedAt.slice(0, 19).replace('T', ' ')}`);
|
|
27
|
+
this.log([header, sep, ...rows].join('\n'));
|
|
28
|
+
this.log('');
|
|
29
|
+
this.log('Restore one with: qfg activity restore <key>');
|
|
30
|
+
return { items };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { JsonObj } from '../../result.js';
|
|
2
|
+
import { APICommand } from '../../index.js';
|
|
3
|
+
export default class ActivityFeed extends APICommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
limit: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<JsonObj | void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { APICommand } from '../../index.js';
|
|
3
|
+
const MS_PER_MINUTE = 60_000;
|
|
4
|
+
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
5
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
6
|
+
/** Compact relative date — "5m ago", "3h ago", "2d ago", or ISO when older. */
|
|
7
|
+
function relativeTime(iso, now = Date.now()) {
|
|
8
|
+
const t = new Date(iso).getTime();
|
|
9
|
+
if (!Number.isFinite(t))
|
|
10
|
+
return iso;
|
|
11
|
+
const diff = now - t;
|
|
12
|
+
if (diff < MS_PER_MINUTE)
|
|
13
|
+
return 'just now';
|
|
14
|
+
if (diff < MS_PER_HOUR)
|
|
15
|
+
return `${Math.floor(diff / MS_PER_MINUTE)}m ago`;
|
|
16
|
+
if (diff < MS_PER_DAY)
|
|
17
|
+
return `${Math.floor(diff / MS_PER_HOUR)}h ago`;
|
|
18
|
+
if (diff < 14 * MS_PER_DAY)
|
|
19
|
+
return `${Math.floor(diff / MS_PER_DAY)}d ago`;
|
|
20
|
+
return iso.slice(0, 10); // YYYY-MM-DD for older entries
|
|
21
|
+
}
|
|
22
|
+
/** Author column — fall back to email when authorName is empty (gitea bots). */
|
|
23
|
+
function authorLabel(item) {
|
|
24
|
+
if (item.authorName && item.authorName.trim().length > 0)
|
|
25
|
+
return item.authorName;
|
|
26
|
+
return item.authorEmail || '(unknown)';
|
|
27
|
+
}
|
|
28
|
+
export default class ActivityFeed extends APICommand {
|
|
29
|
+
static description = `Recent activity across the workspace, newest first.
|
|
30
|
+
|
|
31
|
+
Each entry shows who changed which config, the action (created/updated/deleted/restored),
|
|
32
|
+
and the human-readable audit message produced by the server. The server already
|
|
33
|
+
translates JSON diffs into messages — this command does not re-translate.`;
|
|
34
|
+
static examples = [
|
|
35
|
+
'<%= config.bin %> <%= command.id %>',
|
|
36
|
+
'<%= config.bin %> <%= command.id %> --limit 5',
|
|
37
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
38
|
+
];
|
|
39
|
+
static flags = {
|
|
40
|
+
limit: Flags.integer({
|
|
41
|
+
default: 30,
|
|
42
|
+
description: 'Maximum number of feed entries (1-100)',
|
|
43
|
+
max: 100,
|
|
44
|
+
min: 1,
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
async run() {
|
|
48
|
+
const { flags } = await this.parse(ActivityFeed);
|
|
49
|
+
const request = await this.apiClient.post('/api/v1/activity/getWorkspaceFeed', {
|
|
50
|
+
workspaceId: this.workspaceId,
|
|
51
|
+
limit: flags.limit,
|
|
52
|
+
});
|
|
53
|
+
if (!request.ok) {
|
|
54
|
+
const errorMsg = request.error?.error || `Failed to fetch activity feed: ${request.status}`;
|
|
55
|
+
return this.err(errorMsg, { serverError: request.error });
|
|
56
|
+
}
|
|
57
|
+
const items = (Array.isArray(request.json) ? request.json : []);
|
|
58
|
+
if (items.length === 0) {
|
|
59
|
+
this.log('No recent activity in this workspace.');
|
|
60
|
+
return { items: [] };
|
|
61
|
+
}
|
|
62
|
+
const lines = [];
|
|
63
|
+
for (const item of items) {
|
|
64
|
+
const sha = item.sha.slice(0, 8);
|
|
65
|
+
const who = authorLabel(item);
|
|
66
|
+
const when = relativeTime(item.date);
|
|
67
|
+
const target = item.configType && item.configKey ? `${item.configType}/${item.configKey}` : '(workspace)';
|
|
68
|
+
lines.push(`${sha} ${item.action.padEnd(9)} ${target} by ${who} ${when}`);
|
|
69
|
+
for (const m of item.messages) {
|
|
70
|
+
lines.push(` - ${m.message}`);
|
|
71
|
+
}
|
|
72
|
+
lines.push('');
|
|
73
|
+
}
|
|
74
|
+
this.log(lines.join('\n').trimEnd());
|
|
75
|
+
return { items };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { JsonObj } from '../../result.js';
|
|
2
|
+
import { APICommand } from '../../index.js';
|
|
3
|
+
export default class ActivityHistory extends APICommand {
|
|
4
|
+
static args: {
|
|
5
|
+
name: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
run(): Promise<JsonObj | void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { APICommand } from '../../index.js';
|
|
2
|
+
import { resolveConfigByKey } from '../../util/activity-lookup.js';
|
|
3
|
+
import nameArg from '../../util/name-arg.js';
|
|
4
|
+
function authorLabel(entry) {
|
|
5
|
+
if (entry.authorName && entry.authorName.trim().length > 0)
|
|
6
|
+
return entry.authorName;
|
|
7
|
+
return entry.authorEmail || '(unknown)';
|
|
8
|
+
}
|
|
9
|
+
export default class ActivityHistory extends APICommand {
|
|
10
|
+
static args = { ...nameArg };
|
|
11
|
+
static description = `Per-config audit trail for a flag, config, log-level, segment, or schema.
|
|
12
|
+
|
|
13
|
+
Resolves NAME against the workspace metadata (no need to specify type) and
|
|
14
|
+
returns the full commit history for that file with audit messages translated
|
|
15
|
+
by the server. Restored entries are rendered distinctly so a recovery doesn't
|
|
16
|
+
look like a fresh create.`;
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> <%= command.id %> my.flag',
|
|
19
|
+
'<%= config.bin %> <%= command.id %> request.timeout --json',
|
|
20
|
+
];
|
|
21
|
+
async run() {
|
|
22
|
+
const { args } = await this.parse(ActivityHistory);
|
|
23
|
+
if (!args.name) {
|
|
24
|
+
return this.err('Key is required. Usage: qfg activity history <name>');
|
|
25
|
+
}
|
|
26
|
+
const resolved = await resolveConfigByKey(this, args.name);
|
|
27
|
+
if ('error' in resolved) {
|
|
28
|
+
return this.err(resolved.error);
|
|
29
|
+
}
|
|
30
|
+
const request = await this.apiClient.post('/api/v1/activity/getRichHistory', {
|
|
31
|
+
workspaceId: this.workspaceId,
|
|
32
|
+
configType: resolved.configType,
|
|
33
|
+
configKey: resolved.configKey,
|
|
34
|
+
});
|
|
35
|
+
if (!request.ok) {
|
|
36
|
+
const errorMsg = request.error?.error || `Failed to fetch history: ${request.status}`;
|
|
37
|
+
return this.err(errorMsg, { serverError: request.error });
|
|
38
|
+
}
|
|
39
|
+
const entries = (Array.isArray(request.json) ? request.json : []);
|
|
40
|
+
this.log(`History for ${resolved.configType}/${resolved.configKey}:`);
|
|
41
|
+
this.log('');
|
|
42
|
+
if (entries.length === 0) {
|
|
43
|
+
this.log(' (no commits found)');
|
|
44
|
+
return { configType: resolved.configType, configKey: resolved.configKey, entries: [] };
|
|
45
|
+
}
|
|
46
|
+
const lines = [];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const sha = entry.sha.slice(0, 8);
|
|
49
|
+
const who = authorLabel(entry);
|
|
50
|
+
const date = entry.date.slice(0, 19).replace('T', ' ');
|
|
51
|
+
const marker = entry.action === 'restored' ? '★ restored' : entry.action;
|
|
52
|
+
lines.push(`${sha} ${marker.padEnd(11)} ${date} by ${who}`);
|
|
53
|
+
for (const m of entry.messages) {
|
|
54
|
+
lines.push(` - ${m.message}`);
|
|
55
|
+
}
|
|
56
|
+
lines.push('');
|
|
57
|
+
}
|
|
58
|
+
this.log(lines.join('\n').trimEnd());
|
|
59
|
+
return { configType: resolved.configType, configKey: resolved.configKey, entries };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JsonObj } from '../../result.js';
|
|
2
|
+
import { APICommand } from '../../index.js';
|
|
3
|
+
export default class ActivityRestore extends APICommand {
|
|
4
|
+
static args: {
|
|
5
|
+
name: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
type: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<JsonObj | void>;
|
|
14
|
+
/**
|
|
15
|
+
* When the user omits --type, fall back to listing live metadata so we can
|
|
16
|
+
* still find configType for keys that were *renamed-then-deleted* (live
|
|
17
|
+
* record gone, deletion history exists). Returns the unique config types
|
|
18
|
+
* that match the key — empty when nothing matches.
|
|
19
|
+
*/
|
|
20
|
+
private findCandidateTypes;
|
|
21
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { APICommand } from '../../index.js';
|
|
3
|
+
import { confirmYesNo } from '../../push/confirm.js';
|
|
4
|
+
import { normalizeConfigType, resolveConfigByKey } from '../../util/activity-lookup.js';
|
|
5
|
+
import { getAppUrl } from '../../util/domain-urls.js';
|
|
6
|
+
import isInteractive from '../../util/is-interactive.js';
|
|
7
|
+
import nameArg from '../../util/name-arg.js';
|
|
8
|
+
const RESTORABLE_TYPES = ['feature_flag', 'config', 'log_level', 'segment'];
|
|
9
|
+
export default class ActivityRestore extends APICommand {
|
|
10
|
+
static args = { ...nameArg };
|
|
11
|
+
static description = `Undelete a config that was previously removed.
|
|
12
|
+
|
|
13
|
+
Looks up the most recent deletion for the key (so you see who deleted it and when),
|
|
14
|
+
then asks for confirmation before re-creating the file from the prior content.
|
|
15
|
+
Schemas cannot be restored — use git history instead.`;
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> <%= command.id %> my.flag --yes',
|
|
18
|
+
'<%= config.bin %> <%= command.id %> request.timeout --type config --yes',
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
type: Flags.string({
|
|
22
|
+
description: 'Config type. Optional — inferred from history when omitted.',
|
|
23
|
+
options: ['feature_flag', 'config', 'log_level', 'segment', 'flag', 'log-level'],
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
26
|
+
yes: Flags.boolean({ default: false, description: 'Skip the confirmation prompt' }),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
const { args, flags } = await this.parse(ActivityRestore);
|
|
30
|
+
if (!args.name) {
|
|
31
|
+
return this.err('Key is required. Usage: qfg activity restore <name>');
|
|
32
|
+
}
|
|
33
|
+
const key = args.name;
|
|
34
|
+
// Resolve configType. We try the explicit --type first, then fall back to
|
|
35
|
+
// metadata/list (which only finds *live* configs). For deleted-only keys
|
|
36
|
+
// the user must pass --type since metadata/list won't see them.
|
|
37
|
+
let configType = flags.type ? normalizeConfigType(flags.type) : null;
|
|
38
|
+
if (flags.type && !configType) {
|
|
39
|
+
return this.err(`Unsupported --type "${flags.type}".`);
|
|
40
|
+
}
|
|
41
|
+
if (!configType) {
|
|
42
|
+
const candidates = await this.findCandidateTypes(key);
|
|
43
|
+
if (candidates.length === 0) {
|
|
44
|
+
return this.err(`Cannot infer config type for "${key}". Pass --type=<feature_flag|config|log_level|segment>.`);
|
|
45
|
+
}
|
|
46
|
+
if (candidates.length > 1) {
|
|
47
|
+
return this.err(`Ambiguous: "${key}" has deletion history for multiple types (${candidates.join(', ')}). Pass --type to disambiguate.`);
|
|
48
|
+
}
|
|
49
|
+
configType = candidates[0];
|
|
50
|
+
}
|
|
51
|
+
if (!RESTORABLE_TYPES.includes(configType)) {
|
|
52
|
+
return this.err(`Cannot restore type "${configType}". Restorable types: ${RESTORABLE_TYPES.join(', ')}.`);
|
|
53
|
+
}
|
|
54
|
+
const tombstoneRes = await this.apiClient.post('/api/v1/activity/getDeletionForKey', {
|
|
55
|
+
workspaceId: this.workspaceId,
|
|
56
|
+
configType,
|
|
57
|
+
configKey: key,
|
|
58
|
+
});
|
|
59
|
+
if (!tombstoneRes.ok) {
|
|
60
|
+
const errorMsg = tombstoneRes.error?.error || `Failed to look up deletion: ${tombstoneRes.status}`;
|
|
61
|
+
return this.err(errorMsg, { serverError: tombstoneRes.error });
|
|
62
|
+
}
|
|
63
|
+
const tombstone = tombstoneRes.json;
|
|
64
|
+
if (!tombstone) {
|
|
65
|
+
// The bead phrasing — kept verbatim so the friction-log entries that
|
|
66
|
+
// motivated this work map onto the new error message cleanly.
|
|
67
|
+
return this.err(`${key} is not currently deleted; nothing to restore.`);
|
|
68
|
+
}
|
|
69
|
+
if (!flags.yes) {
|
|
70
|
+
if (!isInteractive(flags)) {
|
|
71
|
+
return this.err(`Refusing to restore ${key} without confirmation. Pass --yes or run interactively.`);
|
|
72
|
+
}
|
|
73
|
+
const shortSha = tombstone.commitSha.slice(0, 8);
|
|
74
|
+
const confirmed = await confirmYesNo(`Restore ${configType}/${key}? Last deleted by ${tombstone.deletedBy} on ${tombstone.deletedAt} (sha ${shortSha}) [y/N] `);
|
|
75
|
+
if (!confirmed) {
|
|
76
|
+
return this.err(`Aborted: restore of ${key} not confirmed.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const restoreRes = await this.apiClient.post('/api/v1/activity/restoreItem', {
|
|
80
|
+
workspaceId: this.workspaceId,
|
|
81
|
+
configType,
|
|
82
|
+
configKey: key,
|
|
83
|
+
});
|
|
84
|
+
if (!restoreRes.ok) {
|
|
85
|
+
const errorMsg = restoreRes.error?.error || `Failed to restore: ${restoreRes.status}`;
|
|
86
|
+
return this.err(errorMsg, { serverError: restoreRes.error });
|
|
87
|
+
}
|
|
88
|
+
const result = restoreRes.json;
|
|
89
|
+
const url = `${getAppUrl()}/workspaces/${this.workspaceId}/flags/${result.configKey}`;
|
|
90
|
+
this.log(`Restored ${result.configType}/${result.configKey}`);
|
|
91
|
+
this.log(` commit: ${result.commitSha.slice(0, 8)}`);
|
|
92
|
+
this.log(` url: ${url}`);
|
|
93
|
+
return {
|
|
94
|
+
configType: result.configType,
|
|
95
|
+
configKey: result.configKey,
|
|
96
|
+
commitSha: result.commitSha,
|
|
97
|
+
url,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* When the user omits --type, fall back to listing live metadata so we can
|
|
102
|
+
* still find configType for keys that were *renamed-then-deleted* (live
|
|
103
|
+
* record gone, deletion history exists). Returns the unique config types
|
|
104
|
+
* that match the key — empty when nothing matches.
|
|
105
|
+
*/
|
|
106
|
+
async findCandidateTypes(key) {
|
|
107
|
+
const resolved = await resolveConfigByKey(this, key);
|
|
108
|
+
if ('configType' in resolved && RESTORABLE_TYPES.includes(resolved.configType)) {
|
|
109
|
+
return [resolved.configType];
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
export default class Activity extends BaseCommand {
|
|
3
|
+
static description = 'Inspect what changed in the workspace (recent commits, per-config history, deletions).';
|
|
4
|
+
static examples = [
|
|
5
|
+
'<%= config.bin %> activity feed',
|
|
6
|
+
'<%= config.bin %> activity history my.flag',
|
|
7
|
+
'<%= config.bin %> activity deleted',
|
|
8
|
+
'<%= config.bin %> activity restore my.flag --yes',
|
|
9
|
+
];
|
|
10
|
+
async run() {
|
|
11
|
+
this.log('Use one of the activity subcommands:');
|
|
12
|
+
this.log(' qfg activity feed [--limit N] [--json] # recent workspace changes');
|
|
13
|
+
this.log(' qfg activity history NAME [--json] # per-config audit trail');
|
|
14
|
+
this.log(' qfg activity deleted [--json] # tombstones for deleted items');
|
|
15
|
+
this.log(' qfg activity restore NAME [--yes] [--json] # undelete a config');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discoverability wrapper. Friction-log entries show agents reaching for
|
|
4
|
+
* `qfg audit-log` (with and without a positional). Dispatch by argument
|
|
5
|
+
* presence so both phrasings land on the right activity subcommand.
|
|
6
|
+
*
|
|
7
|
+
* Strategy A from qfg-d6cn.3: a thin top-level command that re-routes via
|
|
8
|
+
* `this.config.runCommand(...)`. We forward unknown flags as-is so
|
|
9
|
+
* `--limit`, `--json`, etc. flow through to the underlying command.
|
|
10
|
+
*/
|
|
11
|
+
export default class AuditLog extends BaseCommand {
|
|
12
|
+
static args: {
|
|
13
|
+
name: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
14
|
+
};
|
|
15
|
+
static description: string;
|
|
16
|
+
static examples: string[];
|
|
17
|
+
static strict: boolean;
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Discoverability wrapper. Friction-log entries show agents reaching for
|
|
5
|
+
* `qfg audit-log` (with and without a positional). Dispatch by argument
|
|
6
|
+
* presence so both phrasings land on the right activity subcommand.
|
|
7
|
+
*
|
|
8
|
+
* Strategy A from qfg-d6cn.3: a thin top-level command that re-routes via
|
|
9
|
+
* `this.config.runCommand(...)`. We forward unknown flags as-is so
|
|
10
|
+
* `--limit`, `--json`, etc. flow through to the underlying command.
|
|
11
|
+
*/
|
|
12
|
+
export default class AuditLog extends BaseCommand {
|
|
13
|
+
static args = {
|
|
14
|
+
name: Args.string({
|
|
15
|
+
description: 'Optional config key. If present, dispatches to `activity history NAME`; otherwise `activity feed`.',
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
static description = 'Alias: `qfg audit-log` → `qfg activity feed`; `qfg audit-log NAME` → `qfg activity history NAME`.';
|
|
19
|
+
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> my.flag'];
|
|
20
|
+
static strict = false;
|
|
21
|
+
async run() {
|
|
22
|
+
const { args, argv } = await this.parse(AuditLog);
|
|
23
|
+
const passthrough = argv.filter((token) => token.startsWith('-'));
|
|
24
|
+
if (args.name) {
|
|
25
|
+
await this.config.runCommand('activity:history', [args.name, ...passthrough]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await this.config.runCommand('activity:feed', passthrough);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../index.js';
|
|
3
3
|
import { storedConfigJsonSchema } from '../init/schema.js';
|
|
4
|
-
const REFERENCE = `
|
|
4
|
+
export const REFERENCE = `
|
|
5
5
|
────────────────────────────────────────────────────────────
|
|
6
6
|
FILE LAYOUT
|
|
7
7
|
────────────────────────────────────────────────────────────
|
|
@@ -104,6 +104,9 @@ OPERATOR REFERENCE
|
|
|
104
104
|
PROP_LESS_THAN_OR_EQUAL int or double
|
|
105
105
|
PROP_GREATER_THAN int or double
|
|
106
106
|
PROP_GREATER_THAN_OR_EQUAL int or double
|
|
107
|
+
PROP_SEMVER_LESS_THAN string semver comparison; valueToMatch is a semver string like "4.0.9"
|
|
108
|
+
PROP_SEMVER_EQUAL string semver comparison; valueToMatch is a semver string like "4.0.9"
|
|
109
|
+
PROP_SEMVER_GREATER_THAN string semver comparison; valueToMatch is a semver string like "4.0.9"
|
|
107
110
|
PROP_BEFORE string (ISO 8601) date/time comparison
|
|
108
111
|
PROP_AFTER string (ISO 8601)
|
|
109
112
|
PROP_MATCHES string regex match
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discoverability wrapper for `qfg history NAME` — friction logs show agents
|
|
4
|
+
* reaching for this phrasing. Always dispatches to `activity history`.
|
|
5
|
+
*/
|
|
6
|
+
export default class History extends BaseCommand {
|
|
7
|
+
static args: {
|
|
8
|
+
name: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
9
|
+
};
|
|
10
|
+
static description: string;
|
|
11
|
+
static examples: string[];
|
|
12
|
+
static strict: boolean;
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Discoverability wrapper for `qfg history NAME` — friction logs show agents
|
|
5
|
+
* reaching for this phrasing. Always dispatches to `activity history`.
|
|
6
|
+
*/
|
|
7
|
+
export default class History extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
name: Args.string({ description: 'Config key to inspect', required: true }),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Alias: `qfg history NAME` → `qfg activity history NAME`.';
|
|
12
|
+
static examples = ['<%= config.bin %> <%= command.id %> my.flag'];
|
|
13
|
+
static strict = false;
|
|
14
|
+
async run() {
|
|
15
|
+
const { args, argv } = await this.parse(History);
|
|
16
|
+
const passthrough = argv.filter((token) => token.startsWith('-'));
|
|
17
|
+
await this.config.runCommand('activity:history', [args.name, ...passthrough]);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/commands/info.js
CHANGED
|
@@ -201,7 +201,7 @@ Related commands:
|
|
|
201
201
|
// eslint-disable-next-line no-await-in-loop
|
|
202
202
|
const request = await this.apiClient.post('/api/v1/analytics/evaluationStats', {
|
|
203
203
|
workspaceId: this.workspaceId,
|
|
204
|
-
environment: env.
|
|
204
|
+
environment: env.name,
|
|
205
205
|
configKey: key,
|
|
206
206
|
days: 1,
|
|
207
207
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discoverability wrapper. `qfg log` is muscle memory from `git log`; route
|
|
4
|
+
* it to the workspace activity feed.
|
|
5
|
+
*/
|
|
6
|
+
export default class Log extends BaseCommand {
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static strict: boolean;
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discoverability wrapper. `qfg log` is muscle memory from `git log`; route
|
|
4
|
+
* it to the workspace activity feed.
|
|
5
|
+
*/
|
|
6
|
+
export default class Log extends BaseCommand {
|
|
7
|
+
static description = 'Alias: `qfg log` → `qfg activity feed`.';
|
|
8
|
+
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --limit 5'];
|
|
9
|
+
static strict = false;
|
|
10
|
+
async run() {
|
|
11
|
+
const { argv } = await this.parse(Log);
|
|
12
|
+
const passthrough = argv.filter((token) => token.startsWith('-'));
|
|
13
|
+
await this.config.runCommand('activity:feed', passthrough);
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/init/schema.js
CHANGED
|
@@ -6,33 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Derived from the Zod schemas in verify/validate.ts — keep in sync.
|
|
8
8
|
*/
|
|
9
|
+
import { OPERATORS as operators } from '../verify/validate.js';
|
|
9
10
|
const configTypes = ['config', 'feature_flag', 'log_level', 'segment'];
|
|
10
11
|
const valueTypes = ['bool', 'string', 'int', 'double', 'json', 'string_list', 'duration', 'log_level'];
|
|
11
12
|
const logLevels = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];
|
|
12
|
-
const operators = [
|
|
13
|
-
'ALWAYS_TRUE',
|
|
14
|
-
'PROP_IS_ONE_OF',
|
|
15
|
-
'PROP_IS_NOT_ONE_OF',
|
|
16
|
-
'PROP_STARTS_WITH_ONE_OF',
|
|
17
|
-
'PROP_DOES_NOT_START_WITH_ONE_OF',
|
|
18
|
-
'PROP_ENDS_WITH_ONE_OF',
|
|
19
|
-
'PROP_DOES_NOT_END_WITH_ONE_OF',
|
|
20
|
-
'PROP_CONTAINS_ONE_OF',
|
|
21
|
-
'PROP_DOES_NOT_CONTAIN_ONE_OF',
|
|
22
|
-
'PROP_LESS_THAN',
|
|
23
|
-
'PROP_LESS_THAN_OR_EQUAL',
|
|
24
|
-
'PROP_GREATER_THAN',
|
|
25
|
-
'PROP_GREATER_THAN_OR_EQUAL',
|
|
26
|
-
'PROP_BEFORE',
|
|
27
|
-
'PROP_AFTER',
|
|
28
|
-
'PROP_MATCHES',
|
|
29
|
-
'PROP_DOES_NOT_MATCH',
|
|
30
|
-
'IN_SEG',
|
|
31
|
-
'NOT_IN_SEG',
|
|
32
|
-
'IN_INT_RANGE',
|
|
33
|
-
'LOOKUP_KEY_IN',
|
|
34
|
-
'LOOKUP_KEY_NOT_IN',
|
|
35
|
-
];
|
|
36
13
|
// ── Value sub-schemas ──────────────────────────────────────────────────
|
|
37
14
|
const boolValue = {
|
|
38
15
|
type: 'object',
|
package/dist/quonfig.js
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { APICommand } from '../index.js';
|
|
2
|
+
export interface MetadataItem {
|
|
3
|
+
description?: string;
|
|
4
|
+
key: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
type: string;
|
|
7
|
+
valueType: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const ACTIVITY_CONFIG_TYPES: Set<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a user-provided NAME to (configType, configKey) by listing metadata
|
|
13
|
+
* and matching on key. Returns either the matched item or an error string.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the resolution pattern in `cli/src/commands/delete.ts` so callers
|
|
16
|
+
* can pass any of: feature_flag, config, log_level, segment, schema.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveConfigByKey(command: APICommand, key: string): Promise<{
|
|
19
|
+
configType: string;
|
|
20
|
+
configKey: string;
|
|
21
|
+
item: MetadataItem;
|
|
22
|
+
} | {
|
|
23
|
+
error: string;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function normalizeConfigType(input: string): string | null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const ACTIVITY_CONFIG_TYPES = new Set(['feature_flag', 'config', 'log_level', 'segment', 'schema']);
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a user-provided NAME to (configType, configKey) by listing metadata
|
|
4
|
+
* and matching on key. Returns either the matched item or an error string.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the resolution pattern in `cli/src/commands/delete.ts` so callers
|
|
7
|
+
* can pass any of: feature_flag, config, log_level, segment, schema.
|
|
8
|
+
*/
|
|
9
|
+
export async function resolveConfigByKey(command, key) {
|
|
10
|
+
const metaRes = await command.apiClient.post('/api/v1/metadata/list', { workspaceId: command.workspaceId });
|
|
11
|
+
if (!metaRes.ok) {
|
|
12
|
+
return { error: `Failed to look up ${key}: ${metaRes.status} ${JSON.stringify(metaRes.error)}` };
|
|
13
|
+
}
|
|
14
|
+
const items = metaRes.json?.configs ?? [];
|
|
15
|
+
const item = items.find((i) => i.key === key);
|
|
16
|
+
if (!item) {
|
|
17
|
+
return { error: `${key} not found in this workspace.` };
|
|
18
|
+
}
|
|
19
|
+
if (!ACTIVITY_CONFIG_TYPES.has(item.type)) {
|
|
20
|
+
return { error: `Cannot inspect ${key}: unsupported type "${item.type}".` };
|
|
21
|
+
}
|
|
22
|
+
return { configType: item.type, configKey: item.key, item };
|
|
23
|
+
}
|
|
24
|
+
/** Map shorthand --type values onto the server's configType enum. */
|
|
25
|
+
const TYPE_ALIASES = {
|
|
26
|
+
flag: 'feature_flag',
|
|
27
|
+
feature_flag: 'feature_flag',
|
|
28
|
+
config: 'config',
|
|
29
|
+
log_level: 'log_level',
|
|
30
|
+
'log-level': 'log_level',
|
|
31
|
+
segment: 'segment',
|
|
32
|
+
schema: 'schema',
|
|
33
|
+
};
|
|
34
|
+
export function normalizeConfigType(input) {
|
|
35
|
+
return TYPE_ALIASES[input] ?? null;
|
|
36
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* - Value type consistency
|
|
20
20
|
* - Weighted values non-empty and consistent
|
|
21
21
|
*/
|
|
22
|
+
export declare const OPERATORS: readonly ["ALWAYS_TRUE", "PROP_IS_ONE_OF", "PROP_IS_NOT_ONE_OF", "PROP_STARTS_WITH_ONE_OF", "PROP_DOES_NOT_START_WITH_ONE_OF", "PROP_ENDS_WITH_ONE_OF", "PROP_DOES_NOT_END_WITH_ONE_OF", "PROP_CONTAINS_ONE_OF", "PROP_DOES_NOT_CONTAIN_ONE_OF", "PROP_LESS_THAN", "PROP_LESS_THAN_OR_EQUAL", "PROP_GREATER_THAN", "PROP_GREATER_THAN_OR_EQUAL", "PROP_SEMVER_LESS_THAN", "PROP_SEMVER_EQUAL", "PROP_SEMVER_GREATER_THAN", "PROP_BEFORE", "PROP_AFTER", "PROP_MATCHES", "PROP_DOES_NOT_MATCH", "IN_SEG", "NOT_IN_SEG", "IN_INT_RANGE", "LOOKUP_KEY_IN", "LOOKUP_KEY_NOT_IN"];
|
|
22
23
|
export type Severity = 'error' | 'warning';
|
|
23
24
|
export interface ValidationIssue {
|
|
24
25
|
file: string;
|
package/dist/verify/validate.js
CHANGED
|
@@ -28,7 +28,7 @@ const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];
|
|
|
28
28
|
// ── Schemas (subset of app-quonfig config-schemas.ts) ───────────────────
|
|
29
29
|
const ConfigTypeSchema = z.enum(['feature_flag', 'config', 'log_level', 'segment', 'schema']);
|
|
30
30
|
const ValueTypeSchema = z.enum(['bool', 'string', 'int', 'double', 'json', 'string_list', 'duration', 'log_level']);
|
|
31
|
-
const
|
|
31
|
+
export const OPERATORS = [
|
|
32
32
|
'ALWAYS_TRUE',
|
|
33
33
|
'PROP_IS_ONE_OF',
|
|
34
34
|
'PROP_IS_NOT_ONE_OF',
|
|
@@ -54,7 +54,8 @@ const OperatorSchema = z.enum([
|
|
|
54
54
|
'IN_INT_RANGE',
|
|
55
55
|
'LOOKUP_KEY_IN',
|
|
56
56
|
'LOOKUP_KEY_NOT_IN',
|
|
57
|
-
]
|
|
57
|
+
];
|
|
58
|
+
const OperatorSchema = z.enum(OPERATORS);
|
|
58
59
|
const LogLevelSchema = z.enum(LOG_LEVELS);
|
|
59
60
|
const BoolValueSchema = z.object({ type: z.literal('bool'), value: z.boolean() });
|
|
60
61
|
const StringValueSchema = z.object({ type: z.literal('string'), value: z.string() });
|