@lightdash/cli 1.33.0 → 1.35.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.
@@ -2,11 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const common_1 = require("@lightdash/common");
4
4
  const appsDownload_1 = require("./appsDownload");
5
- describe('getDataAppUuidFromReference', () => {
5
+ describe('getDataAppReference', () => {
6
6
  const appUuid = 'd3afc44c-6f0f-4d9f-a267-fb739efa31dd';
7
7
  const projectUuid = 'd4c8dfd2-98c0-4eb2-8395-d924aee62611';
8
8
  it('keeps a bare app UUID unchanged', () => {
9
- expect((0, appsDownload_1.getDataAppUuidFromReference)(appUuid)).toBe(appUuid);
9
+ expect((0, appsDownload_1.getDataAppReference)(appUuid)).toBe(appUuid);
10
+ });
11
+ it('keeps a bare slug unchanged', () => {
12
+ expect((0, appsDownload_1.getDataAppReference)('my-app-slug')).toBe('my-app-slug');
10
13
  });
11
14
  it.each([
12
15
  `https://app.lightdash.cloud/projects/${projectUuid}/apps/${appUuid}`,
@@ -14,7 +17,7 @@ describe('getDataAppUuidFromReference', () => {
14
17
  `https://app.lightdash.cloud/projects/${projectUuid}/apps/${appUuid}/versions/2/view?state=filters#preview`,
15
18
  `https://app.lightdash.cloud/embed/${projectUuid}/app/${appUuid}`,
16
19
  ])('extracts the app UUID from %s', (url) => {
17
- expect((0, appsDownload_1.getDataAppUuidFromReference)(url)).toBe(appUuid);
20
+ expect((0, appsDownload_1.getDataAppReference)(url)).toBe(appUuid);
18
21
  });
19
22
  it.each([
20
23
  'my-app',
@@ -22,7 +25,49 @@ describe('getDataAppUuidFromReference', () => {
22
25
  `https://app.lightdash.cloud/projects/${projectUuid}/apps/generate`,
23
26
  `ftp://app.lightdash.cloud/projects/${projectUuid}/apps/${appUuid}`,
24
27
  ])('leaves unsupported references unchanged: %s', (reference) => {
25
- expect((0, appsDownload_1.getDataAppUuidFromReference)(reference)).toBe(reference);
28
+ expect((0, appsDownload_1.getDataAppReference)(reference)).toBe(reference);
29
+ });
30
+ });
31
+ describe('uploadFilterMatches', () => {
32
+ const makeManifest = (overrides = {}) => ({
33
+ codeVersion: 1,
34
+ appUuid: 'app-uuid-1',
35
+ projectUuid: 'proj-uuid-1',
36
+ version: 3,
37
+ name: 'My App',
38
+ description: '',
39
+ template: null,
40
+ downloadedAt: '2026-06-25T00:00:00.000Z',
41
+ ...overrides,
42
+ });
43
+ it('matches everything when the filter is null', () => {
44
+ expect((0, appsDownload_1.uploadFilterMatches)(null, makeManifest())).toBe(true);
45
+ });
46
+ it('matches on manifest.appUuid', () => {
47
+ const filter = new Set(['app-uuid-1']);
48
+ expect((0, appsDownload_1.uploadFilterMatches)(filter, makeManifest())).toBe(true);
49
+ });
50
+ it('matches on manifest.slug', () => {
51
+ const filter = new Set(['sales-app']);
52
+ expect((0, appsDownload_1.uploadFilterMatches)(filter, makeManifest({ slug: 'sales-app' }))).toBe(true);
53
+ });
54
+ it('does not match when neither uuid nor slug is in the filter', () => {
55
+ const filter = new Set(['other-uuid']);
56
+ expect((0, appsDownload_1.uploadFilterMatches)(filter, makeManifest({ slug: 'sales-app' }))).toBe(false);
57
+ });
58
+ it('matches only by uuid when the manifest has no slug', () => {
59
+ const filter = new Set(['app-uuid-1']);
60
+ expect((0, appsDownload_1.uploadFilterMatches)(filter, makeManifest({ slug: undefined }))).toBe(true);
61
+ expect((0, appsDownload_1.uploadFilterMatches)(new Set(['sales-app']), makeManifest({ slug: undefined }))).toBe(false);
62
+ });
63
+ it('matches only by slug for uuid-free manifests (slug-aware servers)', () => {
64
+ const manifest = makeManifest({
65
+ appUuid: undefined,
66
+ slug: 'sales-app',
67
+ });
68
+ expect((0, appsDownload_1.uploadFilterMatches)(new Set(['sales-app']), manifest)).toBe(true);
69
+ expect((0, appsDownload_1.uploadFilterMatches)(new Set(['app-uuid-1']), manifest)).toBe(false);
70
+ expect((0, appsDownload_1.uploadFilterMatches)(null, manifest)).toBe(true);
26
71
  });
27
72
  });
28
73
  describe('getDataAppUploadFilter', () => {
@@ -62,20 +107,20 @@ const makeDownload = () => ({
62
107
  },
63
108
  });
64
109
  describe('selectAppsToDownload', () => {
65
- it('returns explicit uuids without listing when uuids are passed', () => {
110
+ it('returns explicit refs without listing when refs are passed', () => {
66
111
  expect((0, appsDownload_1.selectAppsToDownload)({ apps: ['uuid-a', 'uuid-b'] })).toEqual({
67
112
  mode: 'explicit',
68
- appUuids: ['uuid-a', 'uuid-b'],
113
+ appRefs: ['uuid-a', 'uuid-b'],
69
114
  });
70
115
  });
71
116
  it('lists all apps when --include-apps is passed', () => {
72
117
  expect((0, appsDownload_1.selectAppsToDownload)({ includeApps: true })).toEqual({
73
118
  mode: 'list-all',
74
- extraAppUuids: [],
119
+ extraAppRefs: [],
75
120
  });
76
121
  });
77
- it('combines --include-apps with explicitly passed uuids', () => {
78
- expect((0, appsDownload_1.selectAppsToDownload)({ apps: ['uuid-a'], includeApps: true })).toEqual({ mode: 'list-all', extraAppUuids: ['uuid-a'] });
122
+ it('combines --include-apps with explicitly passed refs', () => {
123
+ expect((0, appsDownload_1.selectAppsToDownload)({ apps: ['uuid-a'], includeApps: true })).toEqual({ mode: 'list-all', extraAppRefs: ['uuid-a'] });
79
124
  });
80
125
  it('normalizes explicit app URLs without listing apps', () => {
81
126
  expect((0, appsDownload_1.selectAppsToDownload)({
@@ -84,7 +129,7 @@ describe('selectAppsToDownload', () => {
84
129
  ],
85
130
  })).toEqual({
86
131
  mode: 'explicit',
87
- appUuids: ['d3afc44c-6f0f-4d9f-a267-fb739efa31dd'],
132
+ appRefs: ['d3afc44c-6f0f-4d9f-a267-fb739efa31dd'],
88
133
  });
89
134
  });
90
135
  it('skips apps when neither flag is passed', () => {
@@ -170,6 +215,75 @@ describe('ensureDownloadedAppContext', () => {
170
215
  expect(() => (0, appsDownload_1.ensureDownloadedAppContext)('app-uuid-1', withoutContext)).toThrow(/does not support app context downloads/i);
171
216
  });
172
217
  });
218
+ describe('preSlugUploadHint', () => {
219
+ it('suggests adding the slug to lightdash-app.yml when the server returned one', () => {
220
+ const hint = (0, appsDownload_1.preSlugUploadHint)({
221
+ folder: 'lightdash/apps/my-app',
222
+ slug: 'my-app',
223
+ });
224
+ expect(hint).toContain('lightdash/apps/my-app/lightdash-app.yml');
225
+ expect(hint).toContain('predates slug identity');
226
+ expect(hint).toContain('add `slug: my-app` to lightdash-app.yml');
227
+ expect(hint).toContain('Uploads keep working via uuid matching');
228
+ });
229
+ it('omits the add-slug suggestion when the server did not return a slug', () => {
230
+ const hint = (0, appsDownload_1.preSlugUploadHint)({
231
+ folder: 'lightdash/apps/my-app',
232
+ slug: undefined,
233
+ });
234
+ expect(hint).toContain('lightdash/apps/my-app/lightdash-app.yml');
235
+ expect(hint).toContain('predates slug identity');
236
+ expect(hint).not.toContain('add `slug:');
237
+ expect(hint).toContain('Uploads keep working via uuid matching');
238
+ });
239
+ });
240
+ describe('matchedUploadRefs', () => {
241
+ const manifest = {
242
+ codeVersion: 1,
243
+ appUuid: 'app-uuid-1',
244
+ projectUuid: 'proj-uuid-1',
245
+ slug: 'sales-app',
246
+ version: 3,
247
+ name: 'My App',
248
+ description: '',
249
+ template: null,
250
+ downloadedAt: '2026-07-29T00:00:00.000Z',
251
+ };
252
+ it('returns the filter entries the manifest satisfies', () => {
253
+ expect((0, appsDownload_1.matchedUploadRefs)(new Set(['app-uuid-1', 'sales-app', 'other']), manifest)).toEqual(['app-uuid-1', 'sales-app']);
254
+ });
255
+ it('returns only the slug for uuid-free manifests', () => {
256
+ expect((0, appsDownload_1.matchedUploadRefs)(new Set(['app-uuid-1', 'sales-app']), {
257
+ ...manifest,
258
+ appUuid: undefined,
259
+ })).toEqual(['sales-app']);
260
+ });
261
+ });
262
+ describe('unmatchedUploadRefsWarning', () => {
263
+ it('returns null when everything matched', () => {
264
+ expect((0, appsDownload_1.unmatchedUploadRefsWarning)([])).toBeNull();
265
+ });
266
+ it('lists unmatched refs', () => {
267
+ const warning = (0, appsDownload_1.unmatchedUploadRefsWarning)(['sales-ap']);
268
+ expect(warning).toContain('No local app folder matched: sales-ap.');
269
+ expect(warning).not.toContain('slug identity');
270
+ });
271
+ it('adds the slug-identity explanation for uuid-shaped refs', () => {
272
+ const warning = (0, appsDownload_1.unmatchedUploadRefsWarning)([
273
+ 'd3afc44c-6f0f-4d9f-a267-fb739efa31dd',
274
+ ]);
275
+ expect(warning).toContain('carry no uuid');
276
+ expect(warning).toContain('select them by slug');
277
+ });
278
+ });
279
+ describe('preSlugServerHint', () => {
280
+ it('warns that the server matched by uuid only and to check for duplicates', () => {
281
+ const hint = (0, appsDownload_1.preSlugServerHint)('my-app');
282
+ expect(hint).toContain('"my-app"');
283
+ expect(hint).toContain('predates slug-based app identity');
284
+ expect(hint).toContain('verify no duplicate was created');
285
+ });
286
+ });
173
287
  describe('computeUpsertedTotal', () => {
174
288
  it('sums only keys that are neither skipped nor failed', () => {
175
289
  const changes = {
@@ -211,30 +325,6 @@ describe('shouldWarnAllSkipped', () => {
211
325
  expect((0, appsDownload_1.shouldWarnAllSkipped)({})).toBe(false);
212
326
  });
213
327
  });
214
- describe('classifyAppUpload', () => {
215
- it('proceeds when createNew is true regardless of project mismatch', () => {
216
- expect((0, appsDownload_1.classifyAppUpload)('proj-a', 'proj-b', true)).toBe('proceed');
217
- });
218
- it('proceeds when manifest project matches target project', () => {
219
- expect((0, appsDownload_1.classifyAppUpload)('proj-a', 'proj-a', false)).toBe('proceed');
220
- });
221
- it('needs-confirmation when projects differ and createNew is false', () => {
222
- expect((0, appsDownload_1.classifyAppUpload)('proj-a', 'proj-b', false)).toBe('needs-confirmation');
223
- });
224
- });
225
- describe('manifestRetargetHint', () => {
226
- it('names both uuids, the consequence, and the manual fix', () => {
227
- const hint = (0, appsDownload_1.manifestRetargetHint)({
228
- folder: 'my-app',
229
- appUuid: 'new-app-uuid',
230
- projectUuid: 'target-proj-uuid',
231
- });
232
- expect(hint).toContain('my-app/lightdash-app.yml');
233
- expect(hint).toContain('appUuid: new-app-uuid');
234
- expect(hint).toContain('projectUuid: target-proj-uuid');
235
- expect(hint).toMatch(/ask to create again/i);
236
- });
237
- });
238
328
  describe('classifyAppDownloadError', () => {
239
329
  const make404 = (message) => new common_1.LightdashError({
240
330
  message,
@@ -287,7 +377,7 @@ describe('appsDownloadSummary', () => {
287
377
  expect(summary.failureLines).toEqual([]);
288
378
  });
289
379
  it('reports a warning with per-app reasons when any app failed', () => {
290
- const summary = (0, appsDownload_1.appsDownloadSummary)(0, 1, [{ appUuid: 'uuid-a', message: 'server exploded' }], '/tmp/x/apps', 0);
380
+ const summary = (0, appsDownload_1.appsDownloadSummary)(0, 1, [{ appRef: 'uuid-a', message: 'server exploded' }], '/tmp/x/apps', 0);
291
381
  expect(summary.ok).toBe(false);
292
382
  expect(summary.message).toContain('Downloaded 0 of 1 data app(s)');
293
383
  expect(summary.message).toContain('1 failed');
@@ -303,7 +393,7 @@ describe('appsDownloadSummary', () => {
303
393
  expect(summary.failureLines).toEqual([]);
304
394
  });
305
395
  it('reports skips and failures together', () => {
306
- const summary = (0, appsDownload_1.appsDownloadSummary)(1, 3, [{ appUuid: 'uuid-a', message: 'server exploded' }], '/tmp/x/apps', 1);
396
+ const summary = (0, appsDownload_1.appsDownloadSummary)(1, 3, [{ appRef: 'uuid-a', message: 'server exploded' }], '/tmp/x/apps', 1);
307
397
  expect(summary.ok).toBe(false);
308
398
  expect(summary.message).toContain('Downloaded 1 of 2 data app(s)');
309
399
  expect(summary.message).toContain('1 skipped: no built version');
@@ -1 +1 @@
1
- {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,WAAW,EAyBX,WAAW,EAGX,eAAe,EACf,wBAAwB,EAexB,KAAK,WAAW,EACnB,MAAM,mBAAmB,CAAC;AAY3B,OAAO,EAIH,KAAK,0BAA0B,EAClC,MAAM,iCAAiC,CAAC;AAgEzC,OAAO,EACH,sBAAsB,EAGtB,cAAc,EACd,qBAAqB,EAOrB,cAAc,EACd,cAAc,EACd,8BAA8B,EAC9B,yBAAyB,EACzB,YAAY,EACZ,qBAAqB,EACrB,eAAe,EAElB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,sBAAsB,GAAG;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uBAAuB,EAAE,OAAO,CAAC;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uBAAuB,EAAE,OAAO,CAAC;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,0BAA0B,EAAE,OAAO,CAAC;IACpC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,0BAA0B,EAAE,OAAO,CAAC;IACpC,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAIF,QAAA,MAAM,sBAAsB,qDAKzB,IAAI,CACH,sBAAsB,EACtB,YAAY,GAAG,eAAe,GAAG,QAAQ,GAAG,UAAU,CACzD,KAAG,OAEoE,CAAC;AAEzE,QAAA,MAAM,iBAAiB,oIAWpB,IAAI,CACH,sBAAsB,EACpB,YAAY,GACZ,QAAQ,GACR,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,cAAc,GACd,qBAAqB,GACrB,cAAc,GACd,qBAAqB,GACrB,MAAM,CACX,KAAG,OAYuC,CAAC;AAgF5C,KAAK,aAAa,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,QAAA,MAAM,wBAAwB,UACnB,WAAW,oBACA,OAAO,KAC1B,WAQY,CAAC;AAgXhB,KAAK,mBAAmB,GAAG,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC;AAkDjE,eAAO,MAAM,eAAe,QACnB,MAAM,EAAE,QACP,mBAAmB,aACd,MAAM,eACJ,MAAM,eACN,MAAM,gBACN,OAAO,WACZ,OAAO,eACH,OAAO,qBACD,OAAO,eACb,OAAO,eACN,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,KACtC,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,EAAE,WAAW,EAAE,CAAC,CAsI5D,CAAC;AA8DF,QAAA,MAAM,kBAAkB,cACT,MAAM,SACV,MAAM,EAAE,WACN,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SACxB,OAAO,aACH,OAAO,eACL,MAAM,KACpB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6DhC,CAAC;AAEF,eAAO,MAAM,iCAAiC,SAAU,MAAM,KAAG,MAGzC,CAAC;AAQzB;;;;;;GAMG;AACH,QAAA,MAAM,qCAAqC,UAAW,OAAO,KAAG,OAElB,CAAC;AA8E/C,QAAA,MAAM,2BAA2B,gBAChB,MAAM,KACpB,OAAO,CAAC,wBAAwB,EAAE,CAOpC,CAAC;AAEF,QAAA,MAAM,yBAAyB,cAChB,MAAM,SACV,MAAM,EAAE,WACN,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SACxB,OAAO,aACH,OAAO,eACL,MAAM,KACpB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA8EhC,CAAC;AA4FF,QAAA,MAAM,gBAAgB,gBACL,MAAM,KACpB,OAAO,CAAC,WAAW,EAAE,CAOvB,CAAC;AAoOF,eAAO,MAAM,eAAe,YACf,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAkpBd,CAAC;AAiDF,QAAA,MAAM,sBAAsB,WAChB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,0BAA0B,CAAA;CAoBxD,CAAC;AA6WF,QAAA,MAAM,sBAAsB,mBACR,MAAM,EAAE,eACX,MAAM,oBACF,CAAC,eAAe,GAAG;IAAE,aAAa,EAAE,OAAO,CAAA;CAAE,CAAC,EAAE,sBA6BpE,CAAC;AAEF,eAAO,MAAM,aAAa,YACb,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAyvBd,CAAC;AAEF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;CAqBvB,CAAC"}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"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"}
@@ -1082,10 +1082,10 @@ const downloadHandler = async (options) => {
1082
1082
  });
1083
1083
  if (appsSelection.mode !== 'none') {
1084
1084
  output.startItem('Data apps');
1085
- let appUuidsToDownload;
1085
+ let appRefsToDownload;
1086
1086
  let appListingError = null;
1087
1087
  if (appsSelection.mode === 'explicit') {
1088
- appUuidsToDownload = appsSelection.appUuids;
1088
+ appRefsToDownload = appsSelection.appRefs;
1089
1089
  }
1090
1090
  else {
1091
1091
  // List every app in the project (includes apps not in any space)
@@ -1117,14 +1117,14 @@ const downloadHandler = async (options) => {
1117
1117
  if (truncatedCount > 0) {
1118
1118
  globalState_1.default.log(styles.warning(`Found ${listedAppUuids.length} data apps, downloading the first ${appsLimit}. Pass --apps-limit <n> to raise the cap.`));
1119
1119
  }
1120
- appUuidsToDownload = [
1120
+ appRefsToDownload = [
1121
1121
  ...new Set([
1122
1122
  ...cappedAppUuids,
1123
- ...appsSelection.extraAppUuids,
1123
+ ...appsSelection.extraAppRefs,
1124
1124
  ]),
1125
1125
  ];
1126
1126
  }
1127
- if (appUuidsToDownload.length === 0) {
1127
+ if (appRefsToDownload.length === 0) {
1128
1128
  if (appListingError === null) {
1129
1129
  output.completeItem('0 found');
1130
1130
  }
@@ -1133,22 +1133,22 @@ const downloadHandler = async (options) => {
1133
1133
  }
1134
1134
  }
1135
1135
  else {
1136
- output.updateActive(`0 of ${appUuidsToDownload.length} downloaded`);
1136
+ output.updateActive(`0 of ${appRefsToDownload.length} downloaded`);
1137
1137
  const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
1138
1138
  const appsDir = path.join(baseDir, 'apps');
1139
1139
  const takenFolders = new Set();
1140
1140
  let appSuccessCount = 0;
1141
1141
  let appSkippedNotBuiltCount = 0;
1142
1142
  const appFailures = [];
1143
- for (const appUuid of appUuidsToDownload) {
1143
+ for (const appRef of appRefsToDownload) {
1144
1144
  try {
1145
1145
  // eslint-disable-next-line no-await-in-loop
1146
- const code = (0, appsDownload_1.ensureDownloadedAppContext)(appUuid, await (0, apiClient_1.lightdashApi)({
1146
+ const code = (0, appsDownload_1.ensureDownloadedAppContext)(appRef, await (0, apiClient_1.lightdashApi)({
1147
1147
  method: 'GET',
1148
- url: `/api/v1/ee/projects/${projectId}/apps/${appUuid}/download`,
1148
+ url: `/api/v1/ee/projects/${projectId}/apps/${appRef}/download`,
1149
1149
  body: undefined,
1150
1150
  }));
1151
- const folder = (0, appCodeFiles_1.appFolderName)(code.manifest.name, appUuid, takenFolders);
1151
+ const folder = (0, appCodeFiles_1.resolveAppFolderName)(code.manifest, takenFolders);
1152
1152
  takenFolders.add(folder);
1153
1153
  const appDir = path.join(appsDir, folder);
1154
1154
  const manifest = {
@@ -1176,21 +1176,21 @@ const downloadHandler = async (options) => {
1176
1176
  const outcome = (0, appsDownload_1.classifyAppDownloadError)(appErr);
1177
1177
  if (outcome.kind === 'skip-not-built') {
1178
1178
  appSkippedNotBuiltCount += 1;
1179
- globalState_1.default.debug(`> Skipped app ${appUuid}: no built version to download`);
1179
+ globalState_1.default.debug(`> Skipped app ${appRef}: no built version to download`);
1180
1180
  }
1181
1181
  else {
1182
1182
  appFailures.push({
1183
- appUuid,
1183
+ appRef,
1184
1184
  message: outcome.message,
1185
1185
  });
1186
- globalState_1.default.log(styles.error(`Failed to download app ${appUuid}: ${outcome.message}`));
1186
+ globalState_1.default.log(styles.error(`Failed to download app ${appRef}: ${outcome.message}`));
1187
1187
  }
1188
1188
  }
1189
1189
  output.updateActive(`${appSuccessCount +
1190
1190
  appSkippedNotBuiltCount +
1191
- appFailures.length} of ${appUuidsToDownload.length} processed`);
1191
+ appFailures.length} of ${appRefsToDownload.length} processed`);
1192
1192
  }
1193
- const summary = (0, appsDownload_1.appsDownloadSummary)(appSuccessCount, appUuidsToDownload.length, appFailures, appsDir, appSkippedNotBuiltCount);
1193
+ const summary = (0, appsDownload_1.appsDownloadSummary)(appSuccessCount, appRefsToDownload.length, appFailures, appsDir, appSkippedNotBuiltCount);
1194
1194
  output.completeItem(`${appSuccessCount} downloaded${appSkippedNotBuiltCount > 0
1195
1195
  ? `, ${appSkippedNotBuiltCount} skipped`
1196
1196
  : ''}${appFailures.length > 0
@@ -1769,8 +1769,8 @@ const uploadHandler = async (options) => {
1769
1769
  else if (shouldUploadApps) {
1770
1770
  output.startItem('Data apps');
1771
1771
  // --include-apps uploads every folder on disk; explicit references
1772
- // filter folders by their manifest appUuid
1773
- const filterUuids = (0, appsDownload_1.getDataAppUploadFilter)(explicitAppReferences, options.includeApps === true);
1772
+ // filter folders by their manifest slug or appUuid
1773
+ const uploadFilter = (0, appsDownload_1.getDataAppUploadFilter)(explicitAppReferences, options.includeApps === true);
1774
1774
  const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
1775
1775
  const appsDir = path.join(baseDir, 'apps');
1776
1776
  let appFolderEntries;
@@ -1792,43 +1792,19 @@ const uploadHandler = async (options) => {
1792
1792
  if (subDirs.length === 0) {
1793
1793
  globalState_1.default.log(styles.warning(`No app folders found in ${appsDir}.`));
1794
1794
  }
1795
+ const matchedRefs = new Set();
1795
1796
  for (const subDir of subDirs) {
1796
1797
  const folderPath = path.join(appsDir, subDir.name);
1797
1798
  try {
1798
1799
  // eslint-disable-next-line no-await-in-loop
1799
1800
  const code = await (0, appCodeFiles_1.readBundleFromDir)(folderPath);
1800
- if (filterUuids &&
1801
- !filterUuids.has(code.manifest.appUuid)) {
1802
- globalState_1.default.debug(`Skipping app folder "${subDir.name}" (uuid ${code.manifest.appUuid} not in filter)`);
1801
+ if (!(0, appsDownload_1.uploadFilterMatches)(uploadFilter, code.manifest)) {
1802
+ globalState_1.default.debug(`Skipping app folder "${subDir.name}" (not in filter)`);
1803
1803
  // eslint-disable-next-line no-continue
1804
1804
  continue;
1805
1805
  }
1806
- // Guard: cross-project create
1807
- const uploadDecision = (0, appsDownload_1.classifyAppUpload)(code.manifest.projectUuid, projectId, options.createNew === true);
1808
- if (uploadDecision === 'needs-confirmation') {
1809
- if (process.stdin.isTTY && process.stdout.isTTY) {
1810
- // eslint-disable-next-line no-await-in-loop
1811
- const { confirmed } = await inquirer_1.default.prompt([
1812
- {
1813
- type: 'confirm',
1814
- name: 'confirmed',
1815
- message: `"${subDir.name}" was downloaded from project ${code.manifest.projectUuid}, but you are uploading to project ${projectId}. This will CREATE a new app. Continue?`,
1816
- default: false,
1817
- },
1818
- ]);
1819
- if (!confirmed) {
1820
- globalState_1.default.log(`Skipped "${subDir.name}" (cross-project create declined). Pass --create-new to make this explicit. If this app was already moved to the target project, set appUuid and projectUuid in lightdash-app.yml to the moved app instead.`);
1821
- appsSkipped += 1;
1822
- // eslint-disable-next-line no-continue
1823
- continue;
1824
- }
1825
- }
1826
- else {
1827
- globalState_1.default.log(styles.error(`Cannot upload "${subDir.name}": its manifest targets project ${code.manifest.projectUuid} but you are uploading to project ${projectId}. Pass --create-new to create a new app in the target project. If this app was already moved there, set appUuid and projectUuid in lightdash-app.yml to the moved app instead.`));
1828
- appsFailed += 1;
1829
- // eslint-disable-next-line no-continue
1830
- continue;
1831
- }
1806
+ if (uploadFilter) {
1807
+ (0, appsDownload_1.matchedUploadRefs)(uploadFilter, code.manifest).forEach((ref) => matchedRefs.add(ref));
1832
1808
  }
1833
1809
  // Read declared dependencies from the app folder (optional).
1834
1810
  // eslint-disable-next-line no-await-in-loop
@@ -1870,14 +1846,14 @@ const uploadHandler = async (options) => {
1870
1846
  warningLines.forEach((line) => globalState_1.default.log(line));
1871
1847
  if (process.stdin.isTTY && process.stdout.isTTY) {
1872
1848
  // eslint-disable-next-line no-await-in-loop
1873
- const { proceed } = await inquirer_1.default.prompt([
1849
+ const { proceed } = await output.promptWhilePaused(() => inquirer_1.default.prompt([
1874
1850
  {
1875
1851
  type: 'confirm',
1876
1852
  name: 'proceed',
1877
1853
  message: `Upload "${subDir.name}" with custom dependencies?`,
1878
1854
  default: true,
1879
1855
  },
1880
- ]);
1856
+ ]));
1881
1857
  if (!proceed) {
1882
1858
  globalState_1.default.log(`Skipped "${subDir.name}" (custom dependency upload declined).`);
1883
1859
  appsSkipped += 1;
@@ -1897,7 +1873,7 @@ const uploadHandler = async (options) => {
1897
1873
  createNew: options.createNew === true,
1898
1874
  });
1899
1875
  // eslint-disable-next-line no-await-in-loop
1900
- const { appUuid, version, action, warnings } = await (0, apiClient_1.lightdashApi)({
1876
+ const { appUuid, version, action, slug, warnings } = await (0, apiClient_1.lightdashApi)({
1901
1877
  method: 'POST',
1902
1878
  url: `/api/v1/ee/projects/${projectId}/apps/upload`,
1903
1879
  body: JSON.stringify(body),
@@ -1913,42 +1889,20 @@ const uploadHandler = async (options) => {
1913
1889
  }
1914
1890
  const actionLabel = action === 'create' ? 'created' : 'updated';
1915
1891
  globalState_1.default.log(styles.success(`Uploaded "${code.manifest.name}" — ${actionLabel} v${version} (${appUuid}). Building in the background; the app will show "building" until the server finishes.`));
1892
+ if (code.manifest.slug === undefined) {
1893
+ globalState_1.default.log(styles.warning((0, appsDownload_1.preSlugUploadHint)({
1894
+ folder: subDir.name,
1895
+ slug,
1896
+ })));
1897
+ }
1898
+ else if (slug === undefined) {
1899
+ // Bundle sent a slug but the response has none: the
1900
+ // server predates slug identity and matched by uuid
1901
+ // only (slug-only bundles may have just duplicated).
1902
+ globalState_1.default.log(styles.warning((0, appsDownload_1.preSlugServerHint)(subDir.name)));
1903
+ }
1916
1904
  if (action === 'create') {
1917
1905
  globalState_1.default.log(`New app: ${config.context.serverUrl}/projects/${projectId}/apps/${appUuid}`);
1918
- if (process.stdin.isTTY && process.stdout.isTTY) {
1919
- // eslint-disable-next-line no-await-in-loop
1920
- const { retarget } = await inquirer_1.default.prompt([
1921
- {
1922
- type: 'confirm',
1923
- name: 'retarget',
1924
- message: `Update ${subDir.name}/lightdash-app.yml to target the new app? This sets appUuid ${appUuid}, projectUuid ${projectId}, version ${version} — future uploads will update this app.`,
1925
- default: true,
1926
- },
1927
- ]);
1928
- if (retarget) {
1929
- // eslint-disable-next-line no-await-in-loop
1930
- await (0, appCodeFiles_1.retargetManifest)(folderPath, {
1931
- appUuid,
1932
- projectUuid: projectId,
1933
- version,
1934
- });
1935
- globalState_1.default.log(styles.success(`Updated ${subDir.name}/lightdash-app.yml → appUuid ${appUuid}, projectUuid ${projectId}, version ${version}.`));
1936
- }
1937
- else {
1938
- globalState_1.default.log(styles.warning((0, appsDownload_1.manifestRetargetHint)({
1939
- folder: subDir.name,
1940
- appUuid,
1941
- projectUuid: projectId,
1942
- })));
1943
- }
1944
- }
1945
- else {
1946
- globalState_1.default.log(styles.warning((0, appsDownload_1.manifestRetargetHint)({
1947
- folder: subDir.name,
1948
- appUuid,
1949
- projectUuid: projectId,
1950
- })));
1951
- }
1952
1906
  }
1953
1907
  }
1954
1908
  catch (appErr) {
@@ -1962,6 +1916,12 @@ const uploadHandler = async (options) => {
1962
1916
  globalState_1.default.log(styles.error(`Failed to upload app folder "${subDir.name}"${status ? ` [HTTP ${status}]` : ''}: ${(0, common_1.getErrorMessage)(appErr)}${hint}`));
1963
1917
  }
1964
1918
  }
1919
+ if (uploadFilter) {
1920
+ const unmatchedWarning = (0, appsDownload_1.unmatchedUploadRefsWarning)([...uploadFilter].filter((ref) => !matchedRefs.has(ref)));
1921
+ if (unmatchedWarning) {
1922
+ globalState_1.default.log(styles.warning(unmatchedWarning));
1923
+ }
1924
+ }
1965
1925
  if (appsCreated > 0)
1966
1926
  changes['data apps created'] = appsCreated;
1967
1927
  if (appsUpdated > 0)
@@ -1972,6 +1932,13 @@ const uploadHandler = async (options) => {
1972
1932
  changes['data apps skipped'] = appsSkipped;
1973
1933
  const appSummary = summarizeUploadChanges(changesBeforeApps, changes);
1974
1934
  output.completeItem(appSummary.detail, appSummary.variant);
1935
+ if (appsFailed > 0) {
1936
+ // App uploads are fire-and-forget per folder, so failures are
1937
+ // logged and tallied rather than thrown — but the process must
1938
+ // still exit non-zero or CI pipelines read the run as green.
1939
+ globalState_1.default.log(styles.error(`${appsFailed} data app upload(s) failed — see errors above.`));
1940
+ process.exitCode = 1;
1941
+ }
1975
1942
  }
1976
1943
  const end = Date.now();
1977
1944
  await analytics_1.LightdashAnalytics.track({
@@ -7,6 +7,7 @@ const os = tslib_1.__importStar(require("os"));
7
7
  const path = tslib_1.__importStar(require("path"));
8
8
  const vitest_1 = require("vitest");
9
9
  const globalState_1 = tslib_1.__importDefault(require("../globalState"));
10
+ const appsDownload_1 = require("./apps/appsDownload");
10
11
  const apiClient_1 = require("./dbt/apiClient");
11
12
  const download_1 = require("./download");
12
13
  vitest_1.vi.mock('./dbt/apiClient', async (importOriginal) => ({
@@ -44,6 +45,35 @@ describe('hasContentFilters', () => {
44
45
  }))).toBe(false);
45
46
  });
46
47
  });
48
+ describe('data app upload loop filter wiring', () => {
49
+ const makeManifest = (overrides = {}) => ({
50
+ codeVersion: 1,
51
+ appUuid: 'app-uuid-unmatched',
52
+ projectUuid: 'proj-uuid-1',
53
+ version: 1,
54
+ name: 'App',
55
+ description: '',
56
+ template: null,
57
+ downloadedAt: '2026-06-25T00:00:00.000Z',
58
+ ...overrides,
59
+ });
60
+ it('skips a folder matching neither slug nor uuid, and uploads one matched by slug', () => {
61
+ // Mirrors the --apps <appReferences...> wiring in the upload loop:
62
+ // the filter is built once from the CLI-supplied references, then
63
+ // matched per folder via uploadFilterMatches.
64
+ const uploadFilter = (0, appsDownload_1.getDataAppUploadFilter)(['sales-app'], false);
65
+ const unmatchedFolder = makeManifest({
66
+ appUuid: 'other-uuid',
67
+ slug: 'other-slug',
68
+ });
69
+ const matchedBySlugFolder = makeManifest({
70
+ appUuid: 'matched-uuid',
71
+ slug: 'sales-app',
72
+ });
73
+ expect((0, appsDownload_1.uploadFilterMatches)(uploadFilter, unmatchedFolder)).toBe(false);
74
+ expect((0, appsDownload_1.uploadFilterMatches)(uploadFilter, matchedBySlugFolder)).toBe(true);
75
+ });
76
+ });
47
77
  const makeLooseDashboard = (slug, chartSlugs) => ({
48
78
  slug,
49
79
  name: slug,
package/dist/index.js CHANGED
@@ -374,7 +374,7 @@ const downloadCommand = commander_1.program
374
374
  .option('--include-scheduled-deliveries', 'include all scheduled deliveries in the download', false)
375
375
  .option('--include-all', 'include all optional content in the download', false)
376
376
  .option('--strip-pivot-series', 'strip per-value pivot series config from chart YAML for portable downloads', false)
377
- .option('--apps <appReferences...>', 'Download only specified data apps by UUID or URL (enterprise). Works for apps not added to a space.')
377
+ .option('--apps <appReferences...>', 'Download only the specified data apps, by UUID, slug, or app URL (enterprise). Works for apps not added to a space.')
378
378
  .option('--include-apps', "Include all of the project's data apps (enterprise), capped at --apps-limit (default: 50)", false)
379
379
  .option('--apps-limit <number>', 'Maximum number of data apps downloaded by --include-apps or --include-all (default: 50)', undefined)
380
380
  .option('--apps-only', 'Download only data apps (implies --skip-charts --skip-dashboards --skip-spaces). Requires --apps <appReferences...>, --include-apps, or --include-all.', false)
@@ -410,9 +410,9 @@ const uploadCommand = commander_1.program
410
410
  .option('--include-charts', 'Include charts updates when uploading dashboards', false)
411
411
  .option('--validate', 'Validate charts and dashboards after upload', false)
412
412
  .option('--gzip', 'Enable gzip compression for request bodies', false)
413
- .option('--apps <appReferences...>', 'Upload only specified data apps by UUID or URL (enterprise).')
413
+ .option('--apps <appReferences...>', 'Upload only the specified data apps, by UUID, slug, or app URL (enterprise).')
414
414
  .option('--include-apps', 'Upload all app folders on disk (enterprise).', false)
415
- .option('--create-new', 'Always create a new app from the uploaded code instead of updating the app referenced by lightdash-app.yml.', false)
415
+ .option('--create-new', 'Always create a new app from the uploaded code instead of updating the app referenced by lightdash-app.yml. The new app gets a fresh slug.', false)
416
416
  .option('--organization', 'upload all organization-scoped resources without selecting a project', false)
417
417
  .option('--send-invites', 'send invitations to eligible pending users during organization upload', false);
418
418
  uploadCommand.action(withOrganizationMode(download_1.uploadHandler));
@@ -39,6 +39,7 @@ export declare const createContentAsCodeOutput: ({ operation, scope, }: CreateCo
39
39
  updateActive: (detail: string) => void;
40
40
  completeItem: (detail: string, variant?: ContentAsCodeOutputVariant) => void;
41
41
  runItem: <T>({ label, action, detail, variant, }: RunContentAsCodeItemProps<T>) => Promise<T>;
42
+ promptWhilePaused: <T>(prompt: () => Promise<T>) => Promise<T>;
42
43
  prepareForFailureDetails: () => void;
43
44
  complete: (path: string, elapsedSeconds: number) => boolean;
44
45
  fail: (detail: string, elapsedSeconds: number, showFallback: boolean) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"contentAsCodeOutput.d.ts","sourceRoot":"","sources":["../../src/terminal/contentAsCodeOutput.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,MAAM,MAAM,uBAAuB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACnC,SAAS,EAAE,UAAU,GAAG,QAAQ,CAAC;IACjC,KAAK,EAAE,cAAc,GAAG,SAAS,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,uBAAuB,EAAE,CAAC;CACpC,CAAC;AAEF,KAAK,0BAA0B,GAAG,IAAI,CAClC,wBAAwB,EACxB,WAAW,GAAG,OAAO,GAAG,OAAO,CAClC,GAAG;IACA,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,KAAK,yBAAyB,GAAG,IAAI,CACjC,wBAAwB,EACxB,WAAW,GAAG,OAAO,GAAG,gBAAgB,GAAG,OAAO,CACrD,GAAG;IACA,UAAU,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;CACxD,CAAC;AAkBF,eAAO,MAAM,iBAAiB,eAAgB,MAAM,WACR,CAAC;AAE7C,eAAO,MAAM,yBAAyB,mCAInC,IAAI,CAAC,uBAAuB,EAAE,OAAO,GAAG,QAAQ,GAAG,YAAY,CAAC,WAG3B,CAAC;AAczC,eAAO,MAAM,2BAA2B,4DAMrC,0BAA0B,WAe5B,CAAC;AAEF,eAAO,MAAM,0BAA0B,6DAMpC,yBAAyB,WAY3B,CAAC;AAEF,eAAO,MAAM,2BAA2B,uDAMrC,wBAAwB,WAW1B,CAAC;AAEF,eAAO,MAAM,0BAA0B,QAAO,OAOzC,CAAC;AAEN,eAAO,MAAM,yBAAyB,YAAa,MAAM,KAAG,IAM3D,CAAC;AAEF,eAAO,MAAM,2BAA2B,UAC7B,wBAAwB,KAChC,OAOF,CAAC;AAEF,KAAK,8BAA8B,GAAG,IAAI,CACtC,wBAAwB,EACxB,WAAW,GAAG,OAAO,CACxB,CAAC;AAEF,KAAK,yBAAyB,CAAC,CAAC,IAAI;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,0BAA0B,GAAG,SAAS,CAAC;CAClE,CAAC;AAEF,eAAO,MAAM,yBAAyB,0BAGnC,8BAA8B;uBASH,MAAM,WAAW,MAAM;2BAwBnB,MAAM;2BAcxB,MAAM,YACJ,0BAA0B;cAkBjB,CAAC,uCAKrB,yBAAyB,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC;;qBAiBvB,MAAM,kBAAkB,MAAM,KAAG,OAAO;mBAc7C,MAAM,kBACE,MAAM,gBACR,OAAO;CA0BhC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
1
+ {"version":3,"file":"contentAsCodeOutput.d.ts","sourceRoot":"","sources":["../../src/terminal/contentAsCodeOutput.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,MAAM,MAAM,uBAAuB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACnC,SAAS,EAAE,UAAU,GAAG,QAAQ,CAAC;IACjC,KAAK,EAAE,cAAc,GAAG,SAAS,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,uBAAuB,EAAE,CAAC;CACpC,CAAC;AAEF,KAAK,0BAA0B,GAAG,IAAI,CAClC,wBAAwB,EACxB,WAAW,GAAG,OAAO,GAAG,OAAO,CAClC,GAAG;IACA,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,KAAK,yBAAyB,GAAG,IAAI,CACjC,wBAAwB,EACxB,WAAW,GAAG,OAAO,GAAG,gBAAgB,GAAG,OAAO,CACrD,GAAG;IACA,UAAU,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;CACxD,CAAC;AAkBF,eAAO,MAAM,iBAAiB,eAAgB,MAAM,WACR,CAAC;AAE7C,eAAO,MAAM,yBAAyB,mCAInC,IAAI,CAAC,uBAAuB,EAAE,OAAO,GAAG,QAAQ,GAAG,YAAY,CAAC,WAG3B,CAAC;AAczC,eAAO,MAAM,2BAA2B,4DAMrC,0BAA0B,WAe5B,CAAC;AAEF,eAAO,MAAM,0BAA0B,6DAMpC,yBAAyB,WAY3B,CAAC;AAEF,eAAO,MAAM,2BAA2B,uDAMrC,wBAAwB,WAW1B,CAAC;AAEF,eAAO,MAAM,0BAA0B,QAAO,OAOzC,CAAC;AAEN,eAAO,MAAM,yBAAyB,YAAa,MAAM,KAAG,IAM3D,CAAC;AAEF,eAAO,MAAM,2BAA2B,UAC7B,wBAAwB,KAChC,OAOF,CAAC;AAEF,KAAK,8BAA8B,GAAG,IAAI,CACtC,wBAAwB,EACxB,WAAW,GAAG,OAAO,CACxB,CAAC;AAEF,KAAK,yBAAyB,CAAC,CAAC,IAAI;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,0BAA0B,GAAG,SAAS,CAAC;CAClE,CAAC;AAEF,eAAO,MAAM,yBAAyB,0BAGnC,8BAA8B;uBASH,MAAM,WAAW,MAAM;2BAwBnB,MAAM;2BAcxB,MAAM,YACJ,0BAA0B;cAkBjB,CAAC,uCAKrB,yBAAyB,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC;wBAWX,CAAC,UACtB,MAAM,OAAO,CAAC,CAAC,CAAC,KACzB,OAAO,CAAC,CAAC,CAAC;;qBA+BQ,MAAM,kBAAkB,MAAM,KAAG,OAAO;mBAc7C,MAAM,kBACE,MAAM,gBACR,OAAO;CA0BhC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -134,11 +134,35 @@ const createContentAsCodeOutput = ({ operation, scope, }) => {
134
134
  completeItem(detail(value), variant?.(value));
135
135
  return value;
136
136
  };
137
+ // The spinner repaints its line continuously, drawing over interactive
138
+ // prompts rendered beneath it — the prompt becomes invisible and the
139
+ // command looks hung while waiting for input. Stop the spinner for the
140
+ // prompt's lifetime, then resume with the current progress text.
141
+ const promptWhilePaused = async (prompt) => {
142
+ if (!spinner)
143
+ return prompt();
144
+ spinner.stop();
145
+ try {
146
+ return await prompt();
147
+ }
148
+ finally {
149
+ spinner.start(useTree
150
+ ? (0, exports.formatContentAsCodeProgress)({
151
+ operation,
152
+ scope,
153
+ items,
154
+ activeLabel,
155
+ activeDetail,
156
+ })
157
+ : `${runningLabel} ${activeLabel.toLowerCase()}`);
158
+ }
159
+ };
137
160
  return {
138
161
  startItem,
139
162
  updateActive,
140
163
  completeItem,
141
164
  runItem,
165
+ promptWhilePaused,
142
166
  prepareForFailureDetails: () => {
143
167
  if (!useTree && spinner) {
144
168
  spinner.stop();