@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.
Files changed (46) hide show
  1. package/README.md +85 -34
  2. package/dist/commands/init.d.ts +1 -0
  3. package/dist/commands/init.js +18 -0
  4. package/dist/commands/migrate.js +48 -2
  5. package/dist/commands/pull.d.ts +14 -1
  6. package/dist/commands/pull.js +64 -4
  7. package/dist/commands/push.d.ts +53 -0
  8. package/dist/commands/push.js +370 -0
  9. package/dist/commands/workspace/bootstrap.d.ts +7 -0
  10. package/dist/commands/workspace/bootstrap.js +58 -2
  11. package/dist/migrate/local-write.d.ts +8 -2
  12. package/dist/migrate/local-write.js +55 -7
  13. package/dist/migrate/migration-report.d.ts +18 -0
  14. package/dist/migrate/migration-report.js +64 -1
  15. package/dist/migrate/push-to-cloud.d.ts +14 -3
  16. package/dist/migrate/push-to-cloud.js +77 -6
  17. package/dist/migrate/source.d.ts +50 -1
  18. package/dist/migrate/sources/launch/translate.d.ts +27 -1
  19. package/dist/migrate/sources/launch/translate.js +123 -6
  20. package/dist/migrate/sources/launch/types.d.ts +3 -0
  21. package/dist/migrate/sources/launch/zod-to-json-schema.d.ts +9 -0
  22. package/dist/migrate/sources/launch/zod-to-json-schema.js +521 -0
  23. package/dist/migrate/sources/launch.js +52 -7
  24. package/dist/push/bare-path-diff.d.ts +40 -0
  25. package/dist/push/bare-path-diff.js +126 -0
  26. package/dist/push/confirm.d.ts +10 -0
  27. package/dist/push/confirm.js +108 -0
  28. package/dist/push/diff-summary.d.ts +56 -0
  29. package/dist/push/diff-summary.js +115 -0
  30. package/dist/push/identity-check.d.ts +53 -0
  31. package/dist/push/identity-check.js +135 -0
  32. package/dist/push/run-push.d.ts +107 -0
  33. package/dist/push/run-push.js +318 -0
  34. package/dist/util/get-client.js +5 -4
  35. package/dist/util/get-valid-token.d.ts +3 -1
  36. package/dist/util/get-valid-token.js +28 -3
  37. package/dist/util/gitea-api.d.ts +15 -1
  38. package/dist/util/gitea-api.js +1 -0
  39. package/dist/util/gitea-token-storage.d.ts +7 -0
  40. package/dist/util/quonfig-json.d.ts +44 -0
  41. package/dist/util/quonfig-json.js +65 -0
  42. package/dist/verify/validate.js +6 -2
  43. package/oclif.manifest.json +104 -1
  44. package/package.json +3 -3
  45. /package/dist/{migrate/push-strategy.d.ts → util/clone-and-stack-push.d.ts} +0 -0
  46. /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 transformed = transformConfig(raw.newConfig, envIdMap);
51
- const path = getOutputPath(raw.type, raw.key);
52
- return [{ contents: JSON.stringify(transformed, null, 2), path }];
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>;