@quonfig/cli 0.0.15 → 0.0.17
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 +85 -34
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +18 -0
- package/dist/commands/migrate.js +48 -2
- package/dist/commands/pull.d.ts +14 -1
- package/dist/commands/pull.js +64 -4
- package/dist/commands/push.d.ts +53 -0
- package/dist/commands/push.js +370 -0
- package/dist/commands/workspace/bootstrap.d.ts +7 -0
- package/dist/commands/workspace/bootstrap.js +58 -2
- package/dist/migrate/local-write.d.ts +8 -2
- package/dist/migrate/local-write.js +55 -7
- package/dist/migrate/migration-report.d.ts +18 -0
- package/dist/migrate/migration-report.js +64 -1
- package/dist/migrate/push-to-cloud.d.ts +14 -3
- package/dist/migrate/push-to-cloud.js +77 -6
- package/dist/migrate/source.d.ts +50 -1
- package/dist/migrate/sources/launch/translate.d.ts +27 -1
- package/dist/migrate/sources/launch/translate.js +123 -6
- package/dist/migrate/sources/launch/types.d.ts +3 -0
- package/dist/migrate/sources/launch/zod-to-json-schema.d.ts +9 -0
- package/dist/migrate/sources/launch/zod-to-json-schema.js +521 -0
- package/dist/migrate/sources/launch.js +52 -7
- package/dist/push/bare-path-diff.d.ts +40 -0
- package/dist/push/bare-path-diff.js +126 -0
- package/dist/push/confirm.d.ts +10 -0
- package/dist/push/confirm.js +108 -0
- package/dist/push/diff-summary.d.ts +56 -0
- package/dist/push/diff-summary.js +115 -0
- package/dist/push/identity-check.d.ts +53 -0
- package/dist/push/identity-check.js +135 -0
- package/dist/push/run-push.d.ts +107 -0
- package/dist/push/run-push.js +318 -0
- package/dist/util/get-client.js +5 -4
- package/dist/util/get-valid-token.d.ts +3 -1
- package/dist/util/get-valid-token.js +28 -3
- package/dist/util/gitea-api.d.ts +15 -1
- package/dist/util/gitea-api.js +1 -0
- package/dist/util/gitea-token-storage.d.ts +7 -0
- package/dist/util/quonfig-json.d.ts +44 -0
- package/dist/util/quonfig-json.js +65 -0
- package/dist/verify/validate.js +6 -2
- package/oclif.manifest.json +104 -1
- package/package.json +3 -3
- /package/dist/{migrate/push-strategy.d.ts → util/clone-and-stack-push.d.ts} +0 -0
- /package/dist/{migrate/push-strategy.js → util/clone-and-stack-push.js} +0 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars, perfectionist/sort-interfaces, unicorn/explicit-length-check, unicorn/no-lonely-if, unicorn/no-array-callback-reference */
|
|
2
|
+
// Vendored verbatim from launch-migrator/src/zod-to-json-schema.ts to keep a
|
|
3
|
+
// drop-in sync path. Do not reformat — edits here should be mirrored against
|
|
4
|
+
// launch-migrator so the two converters stay behaviorally identical.
|
|
5
|
+
import * as ts from "typescript";
|
|
6
|
+
const JSON_SCHEMA_DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
7
|
+
function unwrapExpression(node) {
|
|
8
|
+
let current = node;
|
|
9
|
+
while (ts.isParenthesizedExpression(current) ||
|
|
10
|
+
ts.isAsExpression(current) ||
|
|
11
|
+
ts.isTypeAssertionExpression(current)) {
|
|
12
|
+
current = current.expression;
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
function parseRegexLiteral(text) {
|
|
17
|
+
if (!text.startsWith("/")) {
|
|
18
|
+
throw new Error(`Expected regex literal, got: ${text}`);
|
|
19
|
+
}
|
|
20
|
+
let lastSlash = -1;
|
|
21
|
+
for (let index = text.length - 1; index > 0; index -= 1) {
|
|
22
|
+
if (text[index] !== "/")
|
|
23
|
+
continue;
|
|
24
|
+
let backslashCount = 0;
|
|
25
|
+
for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
|
|
26
|
+
backslashCount += 1;
|
|
27
|
+
}
|
|
28
|
+
if (backslashCount % 2 === 0) {
|
|
29
|
+
lastSlash = index;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (lastSlash === -1) {
|
|
34
|
+
throw new Error(`Could not parse regex literal: ${text}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
pattern: text.slice(1, lastSlash),
|
|
38
|
+
flags: text.slice(lastSlash + 1),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function isPlainObject(value) {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function evaluateLiteralExpression(node) {
|
|
45
|
+
const current = unwrapExpression(node);
|
|
46
|
+
if (ts.isStringLiteral(current) || ts.isNoSubstitutionTemplateLiteral(current)) {
|
|
47
|
+
return current.text;
|
|
48
|
+
}
|
|
49
|
+
if (ts.isNumericLiteral(current)) {
|
|
50
|
+
return Number(current.text);
|
|
51
|
+
}
|
|
52
|
+
if (current.kind === ts.SyntaxKind.TrueKeyword)
|
|
53
|
+
return true;
|
|
54
|
+
if (current.kind === ts.SyntaxKind.FalseKeyword)
|
|
55
|
+
return false;
|
|
56
|
+
if (current.kind === ts.SyntaxKind.NullKeyword)
|
|
57
|
+
return null;
|
|
58
|
+
if (ts.isPrefixUnaryExpression(current) && current.operator === ts.SyntaxKind.MinusToken) {
|
|
59
|
+
const operand = evaluateLiteralExpression(current.operand);
|
|
60
|
+
if (typeof operand === "number")
|
|
61
|
+
return -operand;
|
|
62
|
+
}
|
|
63
|
+
if (ts.isArrayLiteralExpression(current)) {
|
|
64
|
+
return current.elements.map((element) => {
|
|
65
|
+
if (ts.isSpreadElement(element)) {
|
|
66
|
+
throw new Error("Spread elements are not supported in literal arrays");
|
|
67
|
+
}
|
|
68
|
+
return evaluateLiteralExpression(element);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (ts.isObjectLiteralExpression(current)) {
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const property of current.properties) {
|
|
74
|
+
if (ts.isSpreadAssignment(property) || ts.isShorthandPropertyAssignment(property)) {
|
|
75
|
+
throw new Error("Spread and shorthand properties are not supported in literal objects");
|
|
76
|
+
}
|
|
77
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
78
|
+
throw new Error("Unsupported object literal property");
|
|
79
|
+
}
|
|
80
|
+
const key = getPropertyName(property.name);
|
|
81
|
+
result[key] = evaluateLiteralExpression(property.initializer);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
if (ts.isRegularExpressionLiteral(current)) {
|
|
86
|
+
return parseRegexLiteral(current.getText());
|
|
87
|
+
}
|
|
88
|
+
if (ts.isBinaryExpression(current) && current.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
89
|
+
const left = evaluateLiteralExpression(current.left);
|
|
90
|
+
const right = evaluateLiteralExpression(current.right);
|
|
91
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
92
|
+
return String(left) + String(right);
|
|
93
|
+
}
|
|
94
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
95
|
+
return left + right;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (ts.isIdentifier(current) && current.text === "undefined") {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`Unsupported literal expression: ${current.getText()}`);
|
|
102
|
+
}
|
|
103
|
+
function getPropertyName(name) {
|
|
104
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
105
|
+
return name.text;
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Unsupported property name: ${name.getText()}`);
|
|
108
|
+
}
|
|
109
|
+
function schemaKind(schema) {
|
|
110
|
+
if (schema === false)
|
|
111
|
+
return "never";
|
|
112
|
+
if (!isPlainObject(schema))
|
|
113
|
+
return "unknown";
|
|
114
|
+
if (Array.isArray(schema.anyOf))
|
|
115
|
+
return "union";
|
|
116
|
+
if (schema.const !== undefined)
|
|
117
|
+
return "const";
|
|
118
|
+
if (Array.isArray(schema.enum))
|
|
119
|
+
return "enum";
|
|
120
|
+
const type = schema.type;
|
|
121
|
+
if (typeof type === "string")
|
|
122
|
+
return type;
|
|
123
|
+
return "unknown";
|
|
124
|
+
}
|
|
125
|
+
function ensureObjectSchema(schema) {
|
|
126
|
+
if (schema === false || !isPlainObject(schema) || schema.type !== "object") {
|
|
127
|
+
throw new Error("Expected object schema");
|
|
128
|
+
}
|
|
129
|
+
return schema;
|
|
130
|
+
}
|
|
131
|
+
function makeNullable(schema) {
|
|
132
|
+
if (!isPlainObject(schema)) {
|
|
133
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
134
|
+
}
|
|
135
|
+
const existing = schema.type;
|
|
136
|
+
if (Array.isArray(existing)) {
|
|
137
|
+
return existing.includes("null") ? schema : { ...schema, type: [...existing, "null"] };
|
|
138
|
+
}
|
|
139
|
+
if (typeof existing === "string") {
|
|
140
|
+
return { ...schema, type: [existing, "null"] };
|
|
141
|
+
}
|
|
142
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
143
|
+
}
|
|
144
|
+
function withAnnotation(schema, key, value) {
|
|
145
|
+
if (schema === false) {
|
|
146
|
+
return schema;
|
|
147
|
+
}
|
|
148
|
+
const next = { ...schema };
|
|
149
|
+
next[key] = value;
|
|
150
|
+
return next;
|
|
151
|
+
}
|
|
152
|
+
function applyNumericConstraint(schema, kind, method, value) {
|
|
153
|
+
if (schema === false)
|
|
154
|
+
return schema;
|
|
155
|
+
const next = { ...schema };
|
|
156
|
+
if (kind === "number" || kind === "integer") {
|
|
157
|
+
if (method === "min" || method === "length")
|
|
158
|
+
next.minimum = value;
|
|
159
|
+
if (method === "max" || method === "length")
|
|
160
|
+
next.maximum = value;
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
if (kind === "string") {
|
|
164
|
+
if (method === "min" || method === "length")
|
|
165
|
+
next.minLength = value;
|
|
166
|
+
if (method === "max" || method === "length")
|
|
167
|
+
next.maxLength = value;
|
|
168
|
+
return next;
|
|
169
|
+
}
|
|
170
|
+
if (kind === "array") {
|
|
171
|
+
if (method === "min" || method === "length")
|
|
172
|
+
next.minItems = value;
|
|
173
|
+
if (method === "max" || method === "length")
|
|
174
|
+
next.maxItems = value;
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`Cannot apply ${method}() to ${kind} schema`);
|
|
178
|
+
}
|
|
179
|
+
function applyMethod(receiver, method, args, ctx) {
|
|
180
|
+
const kind = schemaKind(receiver.schema);
|
|
181
|
+
switch (method) {
|
|
182
|
+
case "optional":
|
|
183
|
+
return { ...receiver, optional: true };
|
|
184
|
+
case "nullable":
|
|
185
|
+
return { schema: makeNullable(receiver.schema), optional: receiver.optional };
|
|
186
|
+
case "default": {
|
|
187
|
+
if (args.length === 0)
|
|
188
|
+
throw new Error("default() requires an argument");
|
|
189
|
+
return {
|
|
190
|
+
schema: withAnnotation(receiver.schema, "default", evaluateLiteralExpression(args[0])),
|
|
191
|
+
optional: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
case "describe": {
|
|
195
|
+
if (args.length === 0)
|
|
196
|
+
throw new Error("describe() requires an argument");
|
|
197
|
+
return {
|
|
198
|
+
schema: withAnnotation(receiver.schema, "description", evaluateLiteralExpression(args[0])),
|
|
199
|
+
optional: receiver.optional,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
case "meta": {
|
|
203
|
+
if (args.length === 0)
|
|
204
|
+
throw new Error("meta() requires an argument");
|
|
205
|
+
const meta = evaluateLiteralExpression(args[0]);
|
|
206
|
+
if (!isPlainObject(meta)) {
|
|
207
|
+
throw new Error("meta() expects an object literal");
|
|
208
|
+
}
|
|
209
|
+
let schema = receiver.schema;
|
|
210
|
+
const supportedKeys = new Set(["description", "title"]);
|
|
211
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
212
|
+
if (supportedKeys.has(key)) {
|
|
213
|
+
schema = withAnnotation(schema, key, value);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
ctx.warnings.push(`Ignored unsupported meta field "${key}"`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { schema, optional: receiver.optional };
|
|
220
|
+
}
|
|
221
|
+
case "url":
|
|
222
|
+
if (kind !== "string") {
|
|
223
|
+
throw new Error("url() can only be applied to string schemas");
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
schema: withAnnotation(receiver.schema, "format", "uri"),
|
|
227
|
+
optional: receiver.optional,
|
|
228
|
+
};
|
|
229
|
+
case "email":
|
|
230
|
+
if (kind !== "string") {
|
|
231
|
+
throw new Error("email() can only be applied to string schemas");
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
schema: withAnnotation(receiver.schema, "format", "email"),
|
|
235
|
+
optional: receiver.optional,
|
|
236
|
+
};
|
|
237
|
+
case "uuid":
|
|
238
|
+
if (kind !== "string") {
|
|
239
|
+
throw new Error("uuid() can only be applied to string schemas");
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
schema: withAnnotation(receiver.schema, "format", "uuid"),
|
|
243
|
+
optional: receiver.optional,
|
|
244
|
+
};
|
|
245
|
+
case "datetime":
|
|
246
|
+
if (kind !== "string") {
|
|
247
|
+
throw new Error("datetime() can only be applied to string schemas");
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
schema: withAnnotation(receiver.schema, "format", "date-time"),
|
|
251
|
+
optional: receiver.optional,
|
|
252
|
+
};
|
|
253
|
+
case "regex": {
|
|
254
|
+
if (kind !== "string") {
|
|
255
|
+
throw new Error("regex() can only be applied to string schemas");
|
|
256
|
+
}
|
|
257
|
+
if (args.length === 0)
|
|
258
|
+
throw new Error("regex() requires an argument");
|
|
259
|
+
const regex = evaluateLiteralExpression(args[0]);
|
|
260
|
+
if (!isPlainObject(regex) || typeof regex.pattern !== "string") {
|
|
261
|
+
throw new Error("regex() expects a regex literal");
|
|
262
|
+
}
|
|
263
|
+
if (typeof regex.flags === "string" && regex.flags.length > 0) {
|
|
264
|
+
ctx.warnings.push(`Ignored regex flags "${regex.flags}"`);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
schema: withAnnotation(receiver.schema, "pattern", regex.pattern),
|
|
268
|
+
optional: receiver.optional,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
case "min":
|
|
272
|
+
if (args.length === 0)
|
|
273
|
+
throw new Error("min() requires an argument");
|
|
274
|
+
return {
|
|
275
|
+
schema: applyNumericConstraint(receiver.schema, kind, "min", Number(evaluateLiteralExpression(args[0]))),
|
|
276
|
+
optional: receiver.optional,
|
|
277
|
+
};
|
|
278
|
+
case "max":
|
|
279
|
+
if (args.length === 0)
|
|
280
|
+
throw new Error("max() requires an argument");
|
|
281
|
+
return {
|
|
282
|
+
schema: applyNumericConstraint(receiver.schema, kind, "max", Number(evaluateLiteralExpression(args[0]))),
|
|
283
|
+
optional: receiver.optional,
|
|
284
|
+
};
|
|
285
|
+
case "length":
|
|
286
|
+
if (args.length === 0)
|
|
287
|
+
throw new Error("length() requires an argument");
|
|
288
|
+
return {
|
|
289
|
+
schema: applyNumericConstraint(receiver.schema, kind, "length", Number(evaluateLiteralExpression(args[0]))),
|
|
290
|
+
optional: receiver.optional,
|
|
291
|
+
};
|
|
292
|
+
case "int": {
|
|
293
|
+
if (kind !== "number" && kind !== "integer") {
|
|
294
|
+
throw new Error("int() can only be applied to number schemas");
|
|
295
|
+
}
|
|
296
|
+
const next = withAnnotation(receiver.schema, "type", "integer");
|
|
297
|
+
return { schema: next, optional: receiver.optional };
|
|
298
|
+
}
|
|
299
|
+
case "positive": {
|
|
300
|
+
if (kind !== "number" && kind !== "integer") {
|
|
301
|
+
throw new Error("positive() can only be applied to number schemas");
|
|
302
|
+
}
|
|
303
|
+
const next = withAnnotation(receiver.schema, "exclusiveMinimum", 0);
|
|
304
|
+
return { schema: next, optional: receiver.optional };
|
|
305
|
+
}
|
|
306
|
+
case "strict": {
|
|
307
|
+
const objectSchema = ensureObjectSchema(receiver.schema);
|
|
308
|
+
return {
|
|
309
|
+
schema: { ...objectSchema, additionalProperties: false },
|
|
310
|
+
optional: receiver.optional,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
case "partial": {
|
|
314
|
+
// .partial() makes all declared properties optional. For records
|
|
315
|
+
// (open-shape objects) there's nothing to do.
|
|
316
|
+
if (!isPlainObject(receiver.schema)) {
|
|
317
|
+
return { ...receiver };
|
|
318
|
+
}
|
|
319
|
+
const { required: _required, ...rest } = receiver.schema;
|
|
320
|
+
return { schema: rest, optional: receiver.optional };
|
|
321
|
+
}
|
|
322
|
+
case "array":
|
|
323
|
+
return {
|
|
324
|
+
schema: {
|
|
325
|
+
type: "array",
|
|
326
|
+
items: receiver.schema,
|
|
327
|
+
},
|
|
328
|
+
optional: receiver.optional,
|
|
329
|
+
};
|
|
330
|
+
case "or": {
|
|
331
|
+
if (args.length === 0)
|
|
332
|
+
throw new Error("or() requires an argument");
|
|
333
|
+
const other = convertExpression(args[0], ctx);
|
|
334
|
+
return {
|
|
335
|
+
schema: { anyOf: [receiver.schema, other.schema] },
|
|
336
|
+
optional: receiver.optional || other.optional,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
default:
|
|
340
|
+
throw new Error(`Unsupported Zod method: ${method}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function convertObjectShape(objectLiteral, ctx) {
|
|
344
|
+
const properties = {};
|
|
345
|
+
const required = [];
|
|
346
|
+
for (const property of objectLiteral.properties) {
|
|
347
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
348
|
+
throw new Error("Only property assignments are supported in z.object()");
|
|
349
|
+
}
|
|
350
|
+
const key = getPropertyName(property.name);
|
|
351
|
+
const child = convertExpression(property.initializer, ctx);
|
|
352
|
+
properties[key] = child.schema;
|
|
353
|
+
if (!child.optional) {
|
|
354
|
+
required.push(key);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties,
|
|
360
|
+
required,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function convertBaseZodCall(method, args, ctx) {
|
|
364
|
+
switch (method) {
|
|
365
|
+
case "string":
|
|
366
|
+
return { schema: { type: "string" }, optional: false };
|
|
367
|
+
case "number":
|
|
368
|
+
return { schema: { type: "number" }, optional: false };
|
|
369
|
+
case "boolean":
|
|
370
|
+
return { schema: { type: "boolean" }, optional: false };
|
|
371
|
+
case "enum": {
|
|
372
|
+
// Canonical form: z.enum(["a", "b"]). Also tolerate the hand-written
|
|
373
|
+
// typo z.enum("a", "b") by treating variadic args as an implicit array.
|
|
374
|
+
let elements;
|
|
375
|
+
if (args.length === 1 && ts.isArrayLiteralExpression(args[0])) {
|
|
376
|
+
elements = args[0].elements;
|
|
377
|
+
}
|
|
378
|
+
else if (args.length >= 1) {
|
|
379
|
+
elements = args;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
throw new Error("enum() expects at least one argument");
|
|
383
|
+
}
|
|
384
|
+
const values = elements.map((element) => evaluateLiteralExpression(element));
|
|
385
|
+
return { schema: { enum: values }, optional: false };
|
|
386
|
+
}
|
|
387
|
+
case "literal": {
|
|
388
|
+
if (args.length !== 1)
|
|
389
|
+
throw new Error("literal() expects a single argument");
|
|
390
|
+
return { schema: { const: evaluateLiteralExpression(args[0]) }, optional: false };
|
|
391
|
+
}
|
|
392
|
+
case "never":
|
|
393
|
+
return { schema: false, optional: false };
|
|
394
|
+
case "array": {
|
|
395
|
+
if (args.length !== 1) {
|
|
396
|
+
throw new Error("array() expects a single argument");
|
|
397
|
+
}
|
|
398
|
+
const item = convertExpression(args[0], ctx);
|
|
399
|
+
return {
|
|
400
|
+
schema: {
|
|
401
|
+
type: "array",
|
|
402
|
+
items: item.schema,
|
|
403
|
+
},
|
|
404
|
+
optional: false,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
case "object": {
|
|
408
|
+
if (args.length === 0) {
|
|
409
|
+
return { schema: { type: "object" }, optional: false };
|
|
410
|
+
}
|
|
411
|
+
if (args.length !== 1 || !ts.isObjectLiteralExpression(args[0])) {
|
|
412
|
+
throw new Error("object() expects a single object literal argument");
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
schema: convertObjectShape(args[0], ctx),
|
|
416
|
+
optional: false,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
case "record": {
|
|
420
|
+
if (args.length === 0 || args.length > 2) {
|
|
421
|
+
throw new Error("record() expects one or two arguments");
|
|
422
|
+
}
|
|
423
|
+
let keySchema;
|
|
424
|
+
let valueIndex = 0;
|
|
425
|
+
if (args.length === 2) {
|
|
426
|
+
keySchema = convertExpression(args[0], ctx);
|
|
427
|
+
valueIndex = 1;
|
|
428
|
+
}
|
|
429
|
+
const valueSchema = convertExpression(args[valueIndex], ctx);
|
|
430
|
+
const schema = {
|
|
431
|
+
type: "object",
|
|
432
|
+
additionalProperties: valueSchema.schema,
|
|
433
|
+
};
|
|
434
|
+
if (keySchema && isPlainObject(keySchema.schema) && Array.isArray(keySchema.schema.enum)) {
|
|
435
|
+
schema.propertyNames = { enum: keySchema.schema.enum };
|
|
436
|
+
}
|
|
437
|
+
return { schema, optional: false };
|
|
438
|
+
}
|
|
439
|
+
case "union": {
|
|
440
|
+
if (args.length !== 1 || !ts.isArrayLiteralExpression(args[0])) {
|
|
441
|
+
throw new Error("union() expects a single array literal argument");
|
|
442
|
+
}
|
|
443
|
+
const options = args[0].elements.map((element) => convertExpression(element, ctx).schema);
|
|
444
|
+
return { schema: { anyOf: options }, optional: false };
|
|
445
|
+
}
|
|
446
|
+
case "tuple": {
|
|
447
|
+
if (args.length !== 1 || !ts.isArrayLiteralExpression(args[0])) {
|
|
448
|
+
throw new Error("tuple() expects a single array literal argument");
|
|
449
|
+
}
|
|
450
|
+
const items = args[0].elements.map((element) => convertExpression(element, ctx).schema);
|
|
451
|
+
const schema = { type: "array", minItems: items.length, maxItems: items.length };
|
|
452
|
+
if (items.length > 0) {
|
|
453
|
+
schema.prefixItems = items;
|
|
454
|
+
schema.items = false;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
schema.items = false;
|
|
458
|
+
}
|
|
459
|
+
return { schema, optional: false };
|
|
460
|
+
}
|
|
461
|
+
default:
|
|
462
|
+
throw new Error(`Unsupported base Zod call: ${method}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function convertCallExpression(call, ctx) {
|
|
466
|
+
const callee = unwrapExpression(call.expression);
|
|
467
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
468
|
+
const method = callee.name.text;
|
|
469
|
+
const receiverExpr = unwrapExpression(callee.expression);
|
|
470
|
+
if (ts.isIdentifier(receiverExpr)) {
|
|
471
|
+
const base = receiverExpr.text;
|
|
472
|
+
if (base === "z") {
|
|
473
|
+
return convertBaseZodCall(method, call.arguments, ctx);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const receiver = convertExpression(receiverExpr, ctx);
|
|
477
|
+
return applyMethod(receiver, method, call.arguments, ctx);
|
|
478
|
+
}
|
|
479
|
+
if (ts.isIdentifier(callee)) {
|
|
480
|
+
if (callee.text === "z") {
|
|
481
|
+
throw new Error("Unexpected bare z() call");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
throw new Error(`Unsupported call expression: ${call.getText()}`);
|
|
485
|
+
}
|
|
486
|
+
function convertExpression(node, ctx) {
|
|
487
|
+
const expr = unwrapExpression(node);
|
|
488
|
+
if (ts.isCallExpression(expr)) {
|
|
489
|
+
return convertCallExpression(expr, ctx);
|
|
490
|
+
}
|
|
491
|
+
// Tolerate hand-written Zod that omits the parens on .nullable/.optional
|
|
492
|
+
// (a common user typo — property access returns the method function, not a
|
|
493
|
+
// schema, so the intent is always the method call).
|
|
494
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
495
|
+
const method = expr.name.text;
|
|
496
|
+
if (method === "nullable" || method === "optional") {
|
|
497
|
+
const receiver = convertExpression(expr.expression, ctx);
|
|
498
|
+
return applyMethod(receiver, method, ts.factory.createNodeArray([]), ctx);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (ts.isIdentifier(expr) && expr.text === "undefined") {
|
|
502
|
+
return { schema: false, optional: true };
|
|
503
|
+
}
|
|
504
|
+
throw new Error(`Unsupported Zod expression: ${expr.getText()}`);
|
|
505
|
+
}
|
|
506
|
+
export function zodToJsonSchema(source) {
|
|
507
|
+
const sourceFile = ts.createSourceFile("schema.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
508
|
+
const ctx = { warnings: [] };
|
|
509
|
+
const expressionStatement = sourceFile.statements.find(ts.isExpressionStatement);
|
|
510
|
+
if (!expressionStatement) {
|
|
511
|
+
throw new Error("Expected a Zod expression");
|
|
512
|
+
}
|
|
513
|
+
const result = convertExpression(expressionStatement.expression, ctx);
|
|
514
|
+
if (result.schema !== false && isPlainObject(result.schema)) {
|
|
515
|
+
result.schema.$schema = JSON_SCHEMA_DRAFT_2020_12;
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
schema: result.schema,
|
|
519
|
+
warnings: ctx.warnings,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchAllChangeHistory, fetchEnvironments } from './launch/api.js';
|
|
2
|
-
import { getOutputPath, isLegacyLogLevel, slugify, transformConfig } from './launch/translate.js';
|
|
2
|
+
import { InvalidSourceConfigError, getOutputPath, isLegacyLogLevel, slugify, transformConfig } from './launch/translate.js';
|
|
3
3
|
const SOURCE_NAME = 'launch';
|
|
4
|
-
const state = { apiKey: null, envIdMap: null };
|
|
4
|
+
const state = { apiKey: null, droppedOverrides: new Map(), envIdMap: null, skippedConfigs: [] };
|
|
5
5
|
class MissingAuthError extends Error {
|
|
6
6
|
constructor(operation) {
|
|
7
7
|
super(`launch source ${operation} requires validateAuth(apiKey) to be called first (no API key configured).`);
|
|
@@ -42,23 +42,66 @@ function translateImpl(change) {
|
|
|
42
42
|
return [];
|
|
43
43
|
if (typeof raw.type === 'string' && isLegacyLogLevel(raw.type))
|
|
44
44
|
return [];
|
|
45
|
-
if (raw.deleted)
|
|
46
|
-
return [];
|
|
45
|
+
if (raw.deleted) {
|
|
46
|
+
return [{ deleted: true, path: getOutputPath(raw.type, raw.key) }];
|
|
47
|
+
}
|
|
47
48
|
if (!raw.newConfig)
|
|
48
49
|
return [];
|
|
49
50
|
const envIdMap = state.envIdMap ?? {};
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const outputPath = getOutputPath(raw.type, raw.key);
|
|
52
|
+
let transformed;
|
|
53
|
+
try {
|
|
54
|
+
transformed = transformConfig(raw.newConfig, envIdMap, (envId) => {
|
|
55
|
+
let perFlag = state.droppedOverrides.get(envId);
|
|
56
|
+
if (!perFlag) {
|
|
57
|
+
perFlag = new Map();
|
|
58
|
+
state.droppedOverrides.set(envId, perFlag);
|
|
59
|
+
}
|
|
60
|
+
perFlag.set(outputPath, (perFlag.get(outputPath) ?? 0) + 1);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof InvalidSourceConfigError) {
|
|
65
|
+
state.skippedConfigs.push({ key: raw.key, reason: error.message });
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
return [{ contents: JSON.stringify(transformed, null, 2), path: outputPath }];
|
|
71
|
+
}
|
|
72
|
+
function getDroppedOverridesImpl() {
|
|
73
|
+
if (state.droppedOverrides.size === 0)
|
|
74
|
+
return null;
|
|
75
|
+
const byEnv = {};
|
|
76
|
+
let total = 0;
|
|
77
|
+
for (const envId of [...state.droppedOverrides.keys()].sort()) {
|
|
78
|
+
const perFlag = state.droppedOverrides.get(envId);
|
|
79
|
+
const record = {};
|
|
80
|
+
for (const [flagPath, count] of [...perFlag.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
81
|
+
record[flagPath] = count;
|
|
82
|
+
total += count;
|
|
83
|
+
}
|
|
84
|
+
byEnv[envId] = record;
|
|
85
|
+
}
|
|
86
|
+
return { byEnv, total };
|
|
87
|
+
}
|
|
88
|
+
function getSkippedConfigsImpl() {
|
|
89
|
+
if (state.skippedConfigs.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return { entries: [...state.skippedConfigs], total: state.skippedConfigs.length };
|
|
53
92
|
}
|
|
54
93
|
async function validateAuthImpl(apiKey) {
|
|
55
94
|
await fetchEnvironments(apiKey);
|
|
56
95
|
state.apiKey = apiKey;
|
|
96
|
+
state.droppedOverrides = new Map();
|
|
97
|
+
state.skippedConfigs = [];
|
|
57
98
|
}
|
|
58
99
|
export const launchSource = {
|
|
59
100
|
fetchChanges(sinceEpochMs) {
|
|
60
101
|
return fetchChangesImpl(sinceEpochMs);
|
|
61
102
|
},
|
|
103
|
+
getDroppedOverrides: getDroppedOverridesImpl,
|
|
104
|
+
getSkippedConfigs: getSkippedConfigsImpl,
|
|
62
105
|
listEnvironments: listEnvironmentsImpl,
|
|
63
106
|
name: SOURCE_NAME,
|
|
64
107
|
translate: translateImpl,
|
|
@@ -67,4 +110,6 @@ export const launchSource = {
|
|
|
67
110
|
export function __resetLaunchSourceForTests() {
|
|
68
111
|
state.apiKey = null;
|
|
69
112
|
state.envIdMap = null;
|
|
113
|
+
state.droppedOverrides = new Map();
|
|
114
|
+
state.skippedConfigs = [];
|
|
70
115
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bare-path diff helper for `qfg push`.
|
|
3
|
+
*
|
|
4
|
+
* When the user's local dir is NOT a clone of the cloud repo (no `.git/`, or
|
|
5
|
+
* a mismatched origin) we still need to show a Guard 3 diff summary BEFORE
|
|
6
|
+
* asking them to confirm the push. The clone-path implementation uses
|
|
7
|
+
* `git log origin/main..HEAD` — that doesn't work here because there is no
|
|
8
|
+
* origin in the local dir.
|
|
9
|
+
*
|
|
10
|
+
* Strategy: probe-clone the cloud repo into an OS tmpdir, then walk both
|
|
11
|
+
* trees file-by-file and classify each as added / modified / deleted.
|
|
12
|
+
*
|
|
13
|
+
* The caller owns cleanup of `scratchDir`. The real caller (buildRealDeps in
|
|
14
|
+
* `commands/push.ts`) stashes the scratch clone path on a closure so it can
|
|
15
|
+
* delete it in a finally-block after runPush resolves — matching the bare
|
|
16
|
+
* path's existing scratch-clone lifecycle in `run-push.ts`.
|
|
17
|
+
*
|
|
18
|
+
* Intentional non-goals:
|
|
19
|
+
* - No token handling. Caller passes a fully-authenticated `remoteUrl`.
|
|
20
|
+
* - No filtering to "known" workspace dirs (configs/, feature-flags/, ...).
|
|
21
|
+
* We exclude dotfiles/dotdirs (`.git`, `.DS_Store`, `.quonfig-*`) so the
|
|
22
|
+
* scratch clone's own `.git/` does not leak into the diff, but otherwise
|
|
23
|
+
* diff whatever is on disk. This keeps the diff surface consistent with
|
|
24
|
+
* `copyDirMirror` in `commands/push.ts`, which mirrors every non-dotfile
|
|
25
|
+
* into the real push clone.
|
|
26
|
+
*/
|
|
27
|
+
import { FileDelta } from './diff-summary.js';
|
|
28
|
+
export interface BarePathDiffResult {
|
|
29
|
+
deltas: FileDelta[];
|
|
30
|
+
/** Tmp dir holding the probe clone. Caller must rm -rf in a finally block. */
|
|
31
|
+
scratchDir: string;
|
|
32
|
+
/** Total file count in the scratch clone (under the same prefix rules as `deltas`). */
|
|
33
|
+
totalRemoteFiles: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Clone `remoteUrl` into an OS tmp dir, then diff the working tree of `localDir`
|
|
37
|
+
* against the scratch clone's working tree. Returns `FileDelta[]` plus the
|
|
38
|
+
* scratch clone's file count.
|
|
39
|
+
*/
|
|
40
|
+
export declare function computeBarePathDiff(localDir: string, remoteUrl: string): Promise<BarePathDiffResult>;
|