dialekt 0.1.0
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 +62 -0
- package/TESTING.md +66 -0
- package/dist/cli/main.d.mts +1 -0
- package/dist/cli/main.mjs +412 -0
- package/dist/formatters-De4Q-X1d.mjs +577 -0
- package/dist/index.d.mts +329 -0
- package/dist/index.mjs +60 -0
- package/package.json +39 -0
- package/pnpm-workspace.yaml +7 -0
- package/src/adapter/types.test.ts +98 -0
- package/src/adapter/types.ts +73 -0
- package/src/benchmark/metrics.test.ts +180 -0
- package/src/benchmark/metrics.ts +69 -0
- package/src/benchmark/report.test.ts +129 -0
- package/src/benchmark/report.ts +21 -0
- package/src/benchmark/runner.test.ts +162 -0
- package/src/benchmark/runner.ts +27 -0
- package/src/cli/commands/add.test.ts +267 -0
- package/src/cli/commands/add.ts +123 -0
- package/src/cli/commands/benchmark.test.ts +346 -0
- package/src/cli/commands/benchmark.ts +148 -0
- package/src/cli/commands/languages.test.ts +127 -0
- package/src/cli/commands/languages.ts +42 -0
- package/src/cli/commands/missing.test.ts +256 -0
- package/src/cli/commands/missing.ts +88 -0
- package/src/cli/commands/translate.test.ts +384 -0
- package/src/cli/commands/translate.ts +106 -0
- package/src/cli/commands/unused.test.ts +192 -0
- package/src/cli/commands/unused.ts +87 -0
- package/src/cli/commands/validate.test.ts +245 -0
- package/src/cli/commands/validate.ts +96 -0
- package/src/cli/config-resolution.test.ts +99 -0
- package/src/cli/config-resolution.ts +29 -0
- package/src/cli/format.test.ts +117 -0
- package/src/cli/format.ts +205 -0
- package/src/cli/formatters.test.ts +186 -0
- package/src/cli/formatters.ts +350 -0
- package/src/cli/main.ts +31 -0
- package/src/config/define-config.test.ts +66 -0
- package/src/config/define-config.ts +5 -0
- package/src/config/load-config.test.ts +35 -0
- package/src/config/load-config.ts +21 -0
- package/src/config/types.test.ts +101 -0
- package/src/config/types.ts +28 -0
- package/src/index.ts +56 -0
- package/src/keys/flatten.test.ts +111 -0
- package/src/keys/flatten.ts +41 -0
- package/src/sdk/file-io.test.ts +139 -0
- package/src/sdk/file-io.ts +21 -0
- package/src/sdk/node-layer.test.ts +54 -0
- package/src/sdk/node-layer.ts +10 -0
- package/src/sdk/php-array-reader.test.ts +114 -0
- package/src/sdk/php-array-reader.ts +26 -0
- package/src/translation/chunking.test.ts +118 -0
- package/src/translation/chunking.ts +57 -0
- package/src/translation/missing-keys.test.ts +179 -0
- package/src/translation/missing-keys.ts +36 -0
- package/src/translation/model-registry.test.ts +54 -0
- package/src/translation/model-registry.ts +43 -0
- package/src/translation/one-shot-strategy.test.ts +259 -0
- package/src/translation/one-shot-strategy.ts +48 -0
- package/src/translation/orchestrator.test.ts +276 -0
- package/src/translation/orchestrator.ts +83 -0
- package/src/translation/prompt.test.ts +149 -0
- package/src/translation/prompt.ts +42 -0
- package/src/translation/tool-loop-strategy.test.ts +279 -0
- package/src/translation/tool-loop-strategy.ts +68 -0
- package/src/translation/types.test.ts +37 -0
- package/src/translation/types.ts +21 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +7 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Effect, Option } from 'effect';
|
|
3
|
+
import { runMissing, missingCommand } from './missing.js';
|
|
4
|
+
import type { DialektConfig } from '../../config/types.js';
|
|
5
|
+
import type { TranslationAdapter, ResourceRef } from '../../adapter/types.js';
|
|
6
|
+
|
|
7
|
+
describe('runMissing', () => {
|
|
8
|
+
const baseConfig: DialektConfig = {
|
|
9
|
+
sourceLocale: 'en',
|
|
10
|
+
targetLocales: ['de', 'fr'],
|
|
11
|
+
strategy: 'one-shot',
|
|
12
|
+
model: { provider: 'openai', modelId: 'gpt-4o' },
|
|
13
|
+
fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
|
|
14
|
+
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
15
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
16
|
+
adapters: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function makeAdapter(opts: {
|
|
20
|
+
name: string;
|
|
21
|
+
locales?: readonly string[];
|
|
22
|
+
missing?: readonly { resource: ResourceRef; missing: readonly string[] }[];
|
|
23
|
+
}): TranslationAdapter {
|
|
24
|
+
return {
|
|
25
|
+
name: opts.name,
|
|
26
|
+
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
27
|
+
listLocales: () => Effect.succeed(opts.locales ?? ['en', 'de']),
|
|
28
|
+
listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
|
|
29
|
+
readResource: () => Effect.succeed({}),
|
|
30
|
+
writeResource: () => Effect.void,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it('logs missing keys in default format', async () => {
|
|
35
|
+
const logs: string[] = [];
|
|
36
|
+
const adapter = makeAdapter({ name: 'test' });
|
|
37
|
+
const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
|
|
38
|
+
const resource: ResourceRef = { key: 'messages', label: 'messages' };
|
|
39
|
+
|
|
40
|
+
const program = runMissing(
|
|
41
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none() },
|
|
42
|
+
() => Effect.succeed(config),
|
|
43
|
+
() =>
|
|
44
|
+
Effect.succeed([
|
|
45
|
+
{ adapter: 'test', locale: 'de', resource, missing: ['hello', 'bye'] },
|
|
46
|
+
]),
|
|
47
|
+
(msg) => Effect.sync(() => logs.push(msg)),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await Effect.runPromise(program);
|
|
51
|
+
expect(logs).toHaveLength(1);
|
|
52
|
+
const parsed = JSON.parse(logs[0]!);
|
|
53
|
+
expect(parsed).toEqual([
|
|
54
|
+
{ adapter: 'test', locale: 'de', resource: 'messages', key: 'hello' },
|
|
55
|
+
{ adapter: 'test', locale: 'de', resource: 'messages', key: 'bye' },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('outputs JSON when --format json is passed', async () => {
|
|
60
|
+
const logs: string[] = [];
|
|
61
|
+
const adapter = makeAdapter({ name: 'test' });
|
|
62
|
+
const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
|
|
63
|
+
const resource: ResourceRef = { key: 'messages', label: 'messages' };
|
|
64
|
+
|
|
65
|
+
const program = runMissing(
|
|
66
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none(), format: Option.some('json') },
|
|
67
|
+
() => Effect.succeed(config),
|
|
68
|
+
() =>
|
|
69
|
+
Effect.succeed([
|
|
70
|
+
{ adapter: 'test', locale: 'de', resource, missing: ['hello'] },
|
|
71
|
+
]),
|
|
72
|
+
(msg) => Effect.sync(() => logs.push(msg)),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
await Effect.runPromise(program);
|
|
76
|
+
expect(logs).toHaveLength(1);
|
|
77
|
+
const parsed = JSON.parse(logs[0]!);
|
|
78
|
+
expect(parsed).toEqual([
|
|
79
|
+
{ adapter: 'test', locale: 'de', resource: 'messages', key: 'hello' },
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('outputs pretty when --format pretty is passed', async () => {
|
|
84
|
+
const logs: string[] = [];
|
|
85
|
+
const adapter = makeAdapter({ name: 'test' });
|
|
86
|
+
const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
|
|
87
|
+
const resource: ResourceRef = { key: 'messages', label: 'messages' };
|
|
88
|
+
|
|
89
|
+
const program = runMissing(
|
|
90
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none(), format: Option.some('pretty') },
|
|
91
|
+
() => Effect.succeed(config),
|
|
92
|
+
() =>
|
|
93
|
+
Effect.succeed([
|
|
94
|
+
{ adapter: 'test', locale: 'de', resource, missing: ['hello', 'bye'] },
|
|
95
|
+
]),
|
|
96
|
+
(msg) => Effect.sync(() => logs.push(msg)),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await Effect.runPromise(program);
|
|
100
|
+
expect(logs).toHaveLength(1);
|
|
101
|
+
expect(logs[0]).toContain('test');
|
|
102
|
+
expect(logs[0]).toContain('de');
|
|
103
|
+
expect(logs[0]).toContain('hello');
|
|
104
|
+
expect(logs[0]).toContain('bye');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns empty JSON array when nothing is missing', async () => {
|
|
108
|
+
const logs: string[] = [];
|
|
109
|
+
const adapter = makeAdapter({ name: 'test' });
|
|
110
|
+
const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
|
|
111
|
+
|
|
112
|
+
const program = runMissing(
|
|
113
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none() },
|
|
114
|
+
() => Effect.succeed(config),
|
|
115
|
+
() => Effect.succeed([]),
|
|
116
|
+
(msg) => Effect.sync(() => logs.push(msg)),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
await Effect.runPromise(program);
|
|
120
|
+
expect(logs).toHaveLength(1);
|
|
121
|
+
expect(JSON.parse(logs[0]!)).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('handles multiple adapters and multiple locales', async () => {
|
|
125
|
+
const logs: string[] = [];
|
|
126
|
+
const a1 = makeAdapter({ name: 'a1' });
|
|
127
|
+
const a2 = makeAdapter({ name: 'a2', locales: ['en', 'fr'] });
|
|
128
|
+
const config = { ...baseConfig, adapters: [a1, a2] as unknown as DialektConfig['adapters'] };
|
|
129
|
+
const resource: ResourceRef = { key: 'messages', label: 'messages' };
|
|
130
|
+
|
|
131
|
+
const program = runMissing(
|
|
132
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none() },
|
|
133
|
+
() => Effect.succeed(config),
|
|
134
|
+
(a) =>
|
|
135
|
+
Effect.succeed([
|
|
136
|
+
{ adapter: a.name, locale: a.name === 'a1' ? 'de' : 'fr', resource, missing: ['k1'] },
|
|
137
|
+
]),
|
|
138
|
+
(msg) => Effect.sync(() => logs.push(msg)),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await Effect.runPromise(program);
|
|
142
|
+
expect(logs).toHaveLength(1);
|
|
143
|
+
const parsed = JSON.parse(logs[0]!);
|
|
144
|
+
expect(parsed).toHaveLength(2);
|
|
145
|
+
expect(parsed).toContainEqual({ adapter: 'a1', locale: 'de', resource: 'messages', key: 'k1' });
|
|
146
|
+
expect(parsed).toContainEqual({ adapter: 'a2', locale: 'fr', resource: 'messages', key: 'k1' });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('filters adapters by --adapter flag', async () => {
|
|
150
|
+
let queriedAdapter: string | undefined;
|
|
151
|
+
const a1 = makeAdapter({ name: 'a1' });
|
|
152
|
+
const a2 = makeAdapter({ name: 'a2' });
|
|
153
|
+
const config = { ...baseConfig, adapters: [a1, a2] as unknown as DialektConfig['adapters'] };
|
|
154
|
+
|
|
155
|
+
const program = runMissing(
|
|
156
|
+
{ config: './config.ts', adapter: Option.some('a2'), baseLanguage: Option.none(), language: Option.none() },
|
|
157
|
+
() => Effect.succeed(config),
|
|
158
|
+
(a) =>
|
|
159
|
+
Effect.sync(() => {
|
|
160
|
+
queriedAdapter = a.name;
|
|
161
|
+
return [];
|
|
162
|
+
}),
|
|
163
|
+
() => Effect.void,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await Effect.runPromise(program);
|
|
167
|
+
expect(queriedAdapter).toBe('a2');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('fails when configLoader fails', async () => {
|
|
171
|
+
const program = runMissing(
|
|
172
|
+
{ config: './missing.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none() },
|
|
173
|
+
() => Effect.fail(new Error('Config not found')),
|
|
174
|
+
() => Effect.succeed([]),
|
|
175
|
+
() => Effect.void,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('Config not found');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('fails when listLocales fails', async () => {
|
|
182
|
+
const adapter: TranslationAdapter = {
|
|
183
|
+
name: 'broken',
|
|
184
|
+
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
185
|
+
listLocales: () => Effect.fail(new Error('disk error') as never),
|
|
186
|
+
listResources: () => Effect.succeed([]),
|
|
187
|
+
readResource: () => Effect.succeed({}),
|
|
188
|
+
writeResource: () => Effect.void,
|
|
189
|
+
};
|
|
190
|
+
const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
|
|
191
|
+
|
|
192
|
+
const program = runMissing(
|
|
193
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none() },
|
|
194
|
+
() => Effect.succeed(config),
|
|
195
|
+
() => Effect.succeed([]),
|
|
196
|
+
() => Effect.void,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await expect(Effect.runPromise(program)).rejects.toThrow('disk error');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('handles empty adapter list', async () => {
|
|
203
|
+
const logs: string[] = [];
|
|
204
|
+
const config = { ...baseConfig, adapters: [] };
|
|
205
|
+
|
|
206
|
+
const program = runMissing(
|
|
207
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none() },
|
|
208
|
+
() => Effect.succeed(config),
|
|
209
|
+
() => Effect.succeed([]),
|
|
210
|
+
(msg) => Effect.sync(() => logs.push(msg)),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
await Effect.runPromise(program);
|
|
214
|
+
expect(logs).toHaveLength(1);
|
|
215
|
+
expect(JSON.parse(logs[0]!)).toEqual([]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('handles single-locale adapter (no targets)', async () => {
|
|
219
|
+
const logs: string[] = [];
|
|
220
|
+
const adapter = makeAdapter({ name: 'mono', locales: ['en'] });
|
|
221
|
+
const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
|
|
222
|
+
|
|
223
|
+
const program = runMissing(
|
|
224
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none() },
|
|
225
|
+
() => Effect.succeed(config),
|
|
226
|
+
() => Effect.succeed([]),
|
|
227
|
+
(msg) => Effect.sync(() => logs.push(msg)),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await Effect.runPromise(program);
|
|
231
|
+
expect(logs).toHaveLength(1);
|
|
232
|
+
expect(JSON.parse(logs[0]!)).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('produces valid JSON array even with many entries', async () => {
|
|
236
|
+
const logs: string[] = [];
|
|
237
|
+
const adapter = makeAdapter({ name: 'test' });
|
|
238
|
+
const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
|
|
239
|
+
const resource: ResourceRef = { key: 'messages', label: 'messages' };
|
|
240
|
+
|
|
241
|
+
const program = runMissing(
|
|
242
|
+
{ config: './config.ts', adapter: Option.none(), baseLanguage: Option.none(), language: Option.none(), format: Option.some('json') },
|
|
243
|
+
() => Effect.succeed(config),
|
|
244
|
+
() =>
|
|
245
|
+
Effect.succeed([
|
|
246
|
+
{ adapter: 'test', locale: 'de', resource, missing: ['a', 'b', 'c'] },
|
|
247
|
+
]),
|
|
248
|
+
(msg) => Effect.sync(() => logs.push(msg)),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
await Effect.runPromise(program);
|
|
252
|
+
const parsed = JSON.parse(logs[0]!);
|
|
253
|
+
expect(parsed).toHaveLength(3);
|
|
254
|
+
expect(parsed[0]).toMatchObject({ adapter: 'test', locale: 'de', resource: 'messages', key: 'a' });
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Command, Options } from '@effect/cli';
|
|
2
|
+
import { Effect, Console, Option } from 'effect';
|
|
3
|
+
import { loadConfig } from '../../config/load-config.js';
|
|
4
|
+
import { resolveEffectiveConfig } from '../config-resolution.js';
|
|
5
|
+
import { computeMissingKeys } from '../../translation/missing-keys.js';
|
|
6
|
+
import { detectFormat, type OutputFormat } from '../format.js';
|
|
7
|
+
import { formatMissingKeys } from '../formatters.js';
|
|
8
|
+
import type { DialektConfig } from '../../config/types.js';
|
|
9
|
+
import type { TranslationAdapter } from '../../adapter/types.js';
|
|
10
|
+
|
|
11
|
+
export interface MissingFlags {
|
|
12
|
+
readonly config: string;
|
|
13
|
+
readonly adapter: Option.Option<string>;
|
|
14
|
+
readonly baseLanguage: Option.Option<string>;
|
|
15
|
+
readonly language: Option.Option<string>;
|
|
16
|
+
readonly format?: Option.Option<string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MissingKeysEntry {
|
|
20
|
+
readonly adapter: string;
|
|
21
|
+
readonly locale: string;
|
|
22
|
+
readonly resource: { label: string };
|
|
23
|
+
readonly missing: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runMissing(
|
|
27
|
+
flags: MissingFlags,
|
|
28
|
+
configLoader: (path: string) => Effect.Effect<DialektConfig, unknown> = loadConfig,
|
|
29
|
+
missingKeysComputer: (
|
|
30
|
+
adapter: TranslationAdapter,
|
|
31
|
+
sourceLocale: string,
|
|
32
|
+
targetLocales: readonly string[],
|
|
33
|
+
) => Effect.Effect<readonly MissingKeysEntry[], unknown> = computeMissingKeys as unknown as typeof missingKeysComputer,
|
|
34
|
+
logger: (msg: string) => Effect.Effect<void> = (msg: string) => Console.log(msg),
|
|
35
|
+
): Effect.Effect<void, never> {
|
|
36
|
+
return Effect.gen(function* () {
|
|
37
|
+
const loaded = yield* configLoader(flags.config);
|
|
38
|
+
const effective = resolveEffectiveConfig(
|
|
39
|
+
{
|
|
40
|
+
baseLanguage: Option.getOrUndefined(flags.baseLanguage),
|
|
41
|
+
language: Option.isSome(flags.language) ? [flags.language.value] : undefined,
|
|
42
|
+
adapter: Option.getOrUndefined(flags.adapter),
|
|
43
|
+
},
|
|
44
|
+
loaded,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const allEntries: Array<{
|
|
48
|
+
adapter: string;
|
|
49
|
+
locale: string;
|
|
50
|
+
resource: string;
|
|
51
|
+
key: string;
|
|
52
|
+
}> = [];
|
|
53
|
+
|
|
54
|
+
for (const a of effective.adapters) {
|
|
55
|
+
const locales = yield* a.listLocales();
|
|
56
|
+
const sourceLocale = effective.sourceLocale;
|
|
57
|
+
const targets = locales.filter((l) => l !== sourceLocale);
|
|
58
|
+
|
|
59
|
+
const entries = yield* missingKeysComputer(a, sourceLocale, targets);
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
for (const key of entry.missing) {
|
|
62
|
+
allEntries.push({
|
|
63
|
+
adapter: entry.adapter,
|
|
64
|
+
locale: entry.locale,
|
|
65
|
+
resource: entry.resource.label,
|
|
66
|
+
key,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const format = detectFormat(
|
|
73
|
+
flags.format !== undefined
|
|
74
|
+
? (Option.getOrUndefined(flags.format) as OutputFormat | undefined)
|
|
75
|
+
: undefined,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
yield* logger(formatMissingKeys(allEntries, format));
|
|
79
|
+
}).pipe(Effect.mapError((e) => e as never)) as Effect.Effect<void, never, never>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const missingCommand = Command.make('missing', {
|
|
83
|
+
config: Options.text('config').pipe(Options.withDefault('./dialekt.config.ts')),
|
|
84
|
+
adapter: Options.optional(Options.text('adapter')),
|
|
85
|
+
baseLanguage: Options.optional(Options.text('base-language')),
|
|
86
|
+
language: Options.optional(Options.text('language')),
|
|
87
|
+
format: Options.optional(Options.text('format')),
|
|
88
|
+
}, (flags) => runMissing(flags));
|