@swell/cli 2.7.0 → 2.9.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.
@@ -8,6 +8,7 @@ export default class AppDev extends PushAppCommand {
8
8
  'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
9
  port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
10
  'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
+ function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
12
  'storefront-id': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
13
  'storefront-select': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
13
14
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
@@ -17,6 +18,7 @@ export default class AppDev extends PushAppCommand {
17
18
  };
18
19
  static summary: string;
19
20
  functionErrors: Map<string, string>;
21
+ functionFilter: string | null;
20
22
  functionPorts: Map<string, number>;
21
23
  functionProcesses: Map<string, ChildProcess>;
22
24
  tmpDir: string;
@@ -18,6 +18,7 @@ export default class AppDev extends PushAppCommand {
18
18
  'swell app dev --storefront-id <id>',
19
19
  'swell app dev --port 3000',
20
20
  'swell app dev --port 3000 --frontend-port 4000',
21
+ 'swell app dev --function my-function',
21
22
  ];
22
23
  static flags = {
23
24
  'no-push': Flags.boolean({
@@ -30,6 +31,9 @@ export default class AppDev extends PushAppCommand {
30
31
  'frontend-port': Flags.integer({
31
32
  description: 'specify the port for the frontend dev server when running with frontend',
32
33
  }),
34
+ function: Flags.string({
35
+ description: 'run only the named function',
36
+ }),
33
37
  'storefront-id': Flags.string({
34
38
  description: 'for storefront apps, identify a storefront to preview and push theme files to',
35
39
  }),
@@ -48,6 +52,8 @@ export default class AppDev extends PushAppCommand {
48
52
  };
49
53
  static summary = `Run an app in dev mode from your local machine.`;
50
54
  functionErrors = new Map();
55
+ // When set, only this function name is bundled, started, and watched
56
+ functionFilter = null;
51
57
  // All available functions
52
58
  functionPorts = new Map();
53
59
  // Wrangler child processes to kill on cleanup
@@ -62,6 +68,7 @@ export default class AppDev extends PushAppCommand {
62
68
  const { flags } = await this.parse(AppDev);
63
69
  const { port, 'frontend-port': frontendPort } = flags;
64
70
  const noPush = flags['no-push'];
71
+ this.functionFilter = flags.function ?? null;
65
72
  if (!(await this.ensureAppExists(undefined, false))) {
66
73
  return;
67
74
  }
@@ -213,12 +220,18 @@ ENVIRONMENT = "development"
213
220
  if (!config.isRootFunction()) {
214
221
  continue; // Skip if not a root function config
215
222
  }
223
+ if (this.functionFilter && config.name !== this.functionFilter) {
224
+ continue;
225
+ }
216
226
  functions.push(config);
217
227
  }
218
228
  }
219
229
  catch {
220
230
  // functions directory doesn't exist
221
231
  }
232
+ if (this.functionFilter && functions.length === 0) {
233
+ this.error(`Function '${this.functionFilter}' not found in ${style.path('functions/')}.`);
234
+ }
222
235
  return functions;
223
236
  }
224
237
  async logAllFunctions(functions) {
@@ -293,6 +306,9 @@ ENVIRONMENT = "development"
293
306
  if (appConfig?.type !== ConfigType.FUNCTION) {
294
307
  return;
295
308
  }
309
+ if (this.functionFilter && appConfig.name !== this.functionFilter) {
310
+ return;
311
+ }
296
312
  try {
297
313
  const fullPath = path.join(this.appPath, appConfig.filePath);
298
314
  // Re-bundle the function
@@ -10,6 +10,7 @@ export default class AppFrontendDev extends PushAppCommand {
10
10
  'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
11
  port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
12
  'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
+ function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
13
14
  };
14
15
  static orientation: {
15
16
  env: string;
@@ -0,0 +1,49 @@
1
+ import { SwellCommand } from '../../swell-command.js';
2
+ export default class Extensions extends SwellCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static args: {
6
+ identifier: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ app: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
12
+ yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
13
+ };
14
+ static examples: string[];
15
+ run(): Promise<void>;
16
+ protected catch(error: Error): Promise<any>;
17
+ /**
18
+ * List mode is bounded to two parallel round-trip phases regardless of app
19
+ * count: phase 1 fans `/client/apps`, the three settings singletons, and a
20
+ * single `/data/:functions` filter; phase 2 fans `/apps/<id>/configs?type=component`
21
+ * only for apps with a non-empty `extensions[]`.
22
+ *
23
+ * `include` / `aggregate` are NOT used here — the CLI's `node-fetch`
24
+ * transport cannot transmit GET-with-body, and URL-form transmission of
25
+ * those payloads is silently dropped server-side. Verified empirically
26
+ * against the live admin API. Don't reach for those primitives.
27
+ */
28
+ private showList;
29
+ /**
30
+ * Detail mode is bounded to one round-trip's worth of latency: the install
31
+ * record (or `/apps/<id>` fallback), the relevant settings singleton(s),
32
+ * `/data/:functions?where[app_id]=<id>&where[extension]=<extId>`, and
33
+ * `/apps/<id>/configs?type=component` — all in parallel.
34
+ */
35
+ private showDetail;
36
+ /**
37
+ * `extensions` emits a synthesized envelope rather than a raw API record —
38
+ * other inspect subcommands print one upstream record, but here the JSON
39
+ * nests `manifest`, `native_bindings[].record`, `bound.functions[]`, and
40
+ * `bound.components[]` under shared status fields.
41
+ *
42
+ * The `Next steps:` footer mixes runnable shell commands and merchant-UI
43
+ * lines (prefixed `(merchant)`). The structured `action` and `action_owner`
44
+ * fields above give an agent a parseable signal independent of the footer.
45
+ */
46
+ private emitDetail;
47
+ private fetchSafe;
48
+ private printPreamble;
49
+ }
@@ -0,0 +1,424 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { FetchError } from 'node-fetch';
3
+ import { inspectResourceBaseArgs, inspectResourceBaseFlags, } from '../../inspect-resource-command.js';
4
+ import { getCurrentAppSlugId, hasAppContext } from '../../lib/apps/index.js';
5
+ import { resolveInspectScope, ScopeError, } from '../../lib/apps/inspect-scope.js';
6
+ import { HEX24, isObjectId } from '../../lib/apps/object-id.js';
7
+ import { resolveAppId } from '../../lib/apps/resolve.js';
8
+ import { default as localConfig } from '../../lib/config.js';
9
+ import { buildExtensionDetail, buildOrphanDetail, componentMatchesExtension, diffManifestEntries, formatExtensionKey, functionMatchesExtension, listGroupForApp, listGroupForOrphans, listMetaFor, nextStepLines, orphanListMeta, parseExtensionKey, } from '../../lib/inspect/extensions.js';
10
+ import { renderKeyMetaTable } from '../../lib/inspect/table.js';
11
+ import { SwellCommand } from '../../swell-command.js';
12
+ export default class Extensions extends SwellCommand {
13
+ static summary = 'Platform extensions with activation chain.';
14
+ static description = `Lists declared payment, shipping, and tax extensions across deployed apps, with status (activated, not activated, gateway missing, etc.) and bound function/component counts. Pass --app=<slug> or --app=. (current swell.json) to scope.
15
+
16
+ Pass an identifier to view a single extension as a synthesized JSON envelope (manifest + native_bindings + bound functions/components + required vs missing events), followed by a "Next steps" footer mixing runnable commands and merchant-UI instructions (prefixed "(merchant)").
17
+
18
+ Identifier forms:
19
+ app.<app>.<extId> — full paste-back key (list column 1)
20
+ <extId> — requires --app= scope
21
+
22
+ 24-char hex id is not accepted: extensions are a synthesized resource with no canonical record id.
23
+ `;
24
+ static args = { ...inspectResourceBaseArgs };
25
+ static flags = { ...inspectResourceBaseFlags };
26
+ static examples = [
27
+ 'swell inspect extensions',
28
+ 'swell inspect extensions --app=my-app',
29
+ 'swell inspect extensions app.my-app.revolut',
30
+ 'swell inspect extensions revolut --app=my-app',
31
+ 'swell inspect extensions app.my-app.revolut --json',
32
+ 'swell inspect extensions --live',
33
+ ];
34
+ async run() {
35
+ const { args, flags } = await this.parse(Extensions);
36
+ if (flags.json && !args.identifier) {
37
+ this.error('--json is only valid in detail mode. Pass an identifier to inspect a single extension, or drop --json for the list view.', { exit: 1 });
38
+ }
39
+ if (!flags.live) {
40
+ await this.api.setEnv('test');
41
+ }
42
+ const scope = await resolveInspectScope({
43
+ resolveAppId: (slug) => resolveAppId(this.api, slug),
44
+ readCurrentAppSlug: () => getCurrentAppSlugId(),
45
+ }, { app: flags.app });
46
+ if (args.identifier) {
47
+ await this.showDetail(args.identifier, scope, flags);
48
+ return;
49
+ }
50
+ await this.showList(scope, flags);
51
+ }
52
+ async catch(error) {
53
+ if (error instanceof FetchError) {
54
+ const message = `Could not connect to Swell API. Please try again later: ${error.message}`;
55
+ return this.error(message, { exit: 2, code: error.code });
56
+ }
57
+ if (error instanceof ScopeError) {
58
+ return this.error(error.message, { exit: 1 });
59
+ }
60
+ return this.error(error.message, { exit: 1 });
61
+ }
62
+ /**
63
+ * List mode is bounded to two parallel round-trip phases regardless of app
64
+ * count: phase 1 fans `/client/apps`, the three settings singletons, and a
65
+ * single `/data/:functions` filter; phase 2 fans `/apps/<id>/configs?type=component`
66
+ * only for apps with a non-empty `extensions[]`.
67
+ *
68
+ * `include` / `aggregate` are NOT used here — the CLI's `node-fetch`
69
+ * transport cannot transmit GET-with-body, and URL-form transmission of
70
+ * those payloads is silently dropped server-side. Verified empirically
71
+ * against the live admin API. Don't reach for those primitives.
72
+ */
73
+ async showList(scope, flags) {
74
+ const fnQuery = { extension: { $exists: true } };
75
+ if (scope.appId) {
76
+ fnQuery.app_id = scope.appId;
77
+ }
78
+ const [installedAppsResp, payments, shipments, taxes, functionsResp] = await Promise.all([
79
+ this.api.get({ adminPath: '/client/apps' }),
80
+ this.fetchSafe('/data/settings/payments'),
81
+ this.fetchSafe('/data/settings/shipments'),
82
+ this.fetchSafe('/data/settings/taxes'),
83
+ this.api.getAll({ adminPath: '/data/:functions' }, { query: fnQuery }),
84
+ ]);
85
+ const installed = (installedAppsResp?.results ??
86
+ []);
87
+ const appsScoped = scope.appId
88
+ ? installed.filter((a) => a.app_id === scope.appId)
89
+ : installed;
90
+ const appSlugById = {};
91
+ for (const a of appsScoped) {
92
+ const slug = slugFromInstalled(a);
93
+ if (slug)
94
+ appSlugById[a.app_id] = slug;
95
+ }
96
+ // Phase 2: components per app with a non-empty extensions[].
97
+ const appsWithExtensions = appsScoped.filter((a) => (a.app?.extensions ?? []).length > 0);
98
+ const componentResponses = await Promise.all(appsWithExtensions.map((a) => this.api
99
+ .getAll({ adminPath: `/apps/${a.app_id}/configs` }, { query: { type: 'component' } })
100
+ .catch(() => ({ results: [] }))));
101
+ const componentsByAppId = {};
102
+ for (const [idx, a] of appsWithExtensions.entries()) {
103
+ componentsByAppId[a.app_id] =
104
+ componentResponses[idx]?.results ?? [];
105
+ }
106
+ const allFunctions = (functionsResp?.results ?? []);
107
+ // Build per-app, per-extension row state.
108
+ const rowStates = [];
109
+ const orphans = new Map();
110
+ for (const installedApp of appsScoped) {
111
+ const appId = installedApp.app_id;
112
+ const appSlug = appSlugById[appId] ?? appId;
113
+ const declared = installedApp.app?.extensions ?? [];
114
+ const declaredIds = new Set(declared.map((e) => e.id));
115
+ const appFunctions = allFunctions.filter((f) => f.app_id === appId);
116
+ const appComponents = componentsByAppId[appId] ?? [];
117
+ for (const entry of declared) {
118
+ rowStates.push({
119
+ appId,
120
+ appSlug,
121
+ manifest: entry,
122
+ functions: appFunctions.filter((f) => functionMatchesExtension(f, entry.id)),
123
+ components: appComponents.filter((c) => componentMatchesExtension(c, entry.id)),
124
+ });
125
+ }
126
+ for (const fn of appFunctions) {
127
+ if (!fn.extension)
128
+ continue;
129
+ if (declaredIds.has(fn.extension))
130
+ continue;
131
+ const key = `${appId}::${fn.extension}`;
132
+ const existing = orphans.get(key);
133
+ if (existing) {
134
+ existing.functions.push(fn);
135
+ }
136
+ else {
137
+ orphans.set(key, {
138
+ appId,
139
+ appSlug,
140
+ unresolvedId: fn.extension,
141
+ functions: [fn],
142
+ components: [],
143
+ });
144
+ }
145
+ }
146
+ for (const comp of appComponents) {
147
+ const ext = comp.values?.extension;
148
+ if (!ext)
149
+ continue;
150
+ if (declaredIds.has(ext))
151
+ continue;
152
+ const key = `${appId}::${ext}`;
153
+ const existing = orphans.get(key);
154
+ if (existing) {
155
+ existing.components.push(comp);
156
+ }
157
+ else {
158
+ orphans.set(key, {
159
+ appId,
160
+ appSlug,
161
+ unresolvedId: ext,
162
+ functions: [],
163
+ components: [comp],
164
+ });
165
+ }
166
+ }
167
+ }
168
+ // --app=. surfaces local-only extensions as `not deployed` rows.
169
+ if (flags.app === '.') {
170
+ const local = await readLocalManifest();
171
+ const localId = local?.id;
172
+ const localExtensions = local?.extensions ?? [];
173
+ if (localId) {
174
+ const installedForLocal = appsScoped.find((a) => slugFromInstalled(a) === localId || a.app_id === localId);
175
+ const appId = installedForLocal?.app_id ?? localId;
176
+ const deployedIds = new Set((installedForLocal?.app?.extensions ?? []).map((e) => e.id));
177
+ for (const entry of localExtensions) {
178
+ if (deployedIds.has(entry.id))
179
+ continue;
180
+ rowStates.push({
181
+ appId,
182
+ appSlug: localId,
183
+ manifest: entry,
184
+ functions: [],
185
+ components: [],
186
+ notDeployed: true,
187
+ });
188
+ }
189
+ }
190
+ }
191
+ this.printPreamble(flags.live ?? false);
192
+ if (rowStates.length === 0 && orphans.size === 0) {
193
+ this.log();
194
+ this.log(' (no extensions found)');
195
+ this.log();
196
+ return;
197
+ }
198
+ rowStates.sort((a, b) => {
199
+ const cmp = a.appSlug.localeCompare(b.appSlug);
200
+ return cmp === 0 ? a.manifest.id.localeCompare(b.manifest.id) : cmp;
201
+ });
202
+ const rows = rowStates.map((state) => {
203
+ const detail = buildExtensionDetail({
204
+ appId: state.appId,
205
+ appSlug: state.appSlug,
206
+ extId: state.manifest.id,
207
+ manifest: state.manifest,
208
+ notDeployed: state.notDeployed,
209
+ payments,
210
+ shipments,
211
+ taxes,
212
+ functions: state.functions,
213
+ components: state.components,
214
+ });
215
+ return {
216
+ key: formatExtensionKey(state.appSlug, state.manifest.id),
217
+ meta: listMetaFor(detail),
218
+ group: listGroupForApp(state.appSlug),
219
+ };
220
+ });
221
+ const orphanRows = [...orphans.values()]
222
+ .sort((a, b) => {
223
+ const cmp = a.appSlug.localeCompare(b.appSlug);
224
+ return cmp === 0 ? a.unresolvedId.localeCompare(b.unresolvedId) : cmp;
225
+ })
226
+ .map((state) => ({
227
+ key: formatExtensionKey(state.appSlug, state.unresolvedId),
228
+ meta: orphanListMeta(state.functions.length, state.components.length),
229
+ group: listGroupForOrphans(),
230
+ }));
231
+ this.log();
232
+ for (const line of renderKeyMetaTable([...rows, ...orphanRows])) {
233
+ this.log(line);
234
+ }
235
+ this.log();
236
+ this.log('Run "swell inspect extensions <key>" to view an extension.');
237
+ this.log();
238
+ }
239
+ /**
240
+ * Detail mode is bounded to one round-trip's worth of latency: the install
241
+ * record (or `/apps/<id>` fallback), the relevant settings singleton(s),
242
+ * `/data/:functions?where[app_id]=<id>&where[extension]=<extId>`, and
243
+ * `/apps/<id>/configs?type=component` — all in parallel.
244
+ */
245
+ async showDetail(identifier, scope, flags) {
246
+ if (HEX24.test(identifier)) {
247
+ this.error(`Extensions are a synthesized resource with no 24-char id. ` +
248
+ `Pass app.<slug>.<extId> or a bare extension id with --app=.`, { exit: 1 });
249
+ }
250
+ const parsed = parseExtensionKey(identifier);
251
+ let appId;
252
+ let appSlug;
253
+ let extId;
254
+ switch (parsed.kind) {
255
+ case 'slug': {
256
+ const isHex = isObjectId(parsed.appPart);
257
+ appId = isHex
258
+ ? parsed.appPart
259
+ : await resolveAppId(this.api, parsed.appPart);
260
+ appSlug = isHex ? parsed.appPart : parsed.appPart;
261
+ extId = parsed.extId;
262
+ break;
263
+ }
264
+ case 'name': {
265
+ if (!scope.appId) {
266
+ this.error(`Bare extension id '${identifier}' requires --app=<slug> or --app=. to scope. ` +
267
+ `Alternatively, pass a full key (app.<slug>.<extId>).`, { exit: 1 });
268
+ }
269
+ appId = scope.appId;
270
+ appSlug = scope.appSlug ?? scope.appId;
271
+ extId = parsed.name;
272
+ break;
273
+ }
274
+ case 'invalid': {
275
+ this.error(`Invalid extension identifier '${identifier}'. ` +
276
+ `Expected bare id or app.<slug>.<extId>.`, { exit: 1 });
277
+ }
278
+ }
279
+ const [installedAppsResp, payments, shipments, taxes, functionsResp] = await Promise.all([
280
+ this.api.get({ adminPath: '/client/apps' }, { query: {} }),
281
+ this.fetchSafe('/data/settings/payments'),
282
+ this.fetchSafe('/data/settings/shipments'),
283
+ this.fetchSafe('/data/settings/taxes'),
284
+ this.api.getAll({ adminPath: '/data/:functions' }, { query: { app_id: appId, extension: extId } }),
285
+ ]);
286
+ const installed = (installedAppsResp?.results ??
287
+ []);
288
+ const installedApp = installed.find((a) => a.app_id === appId);
289
+ let manifest = installedApp?.app?.extensions?.find((e) => e.id === extId) ?? null;
290
+ // Fallback to /apps/<id> if the install record's nested manifest is absent.
291
+ if (!manifest) {
292
+ const fallback = await this.api
293
+ .get({ adminPath: `/apps/${appId}` })
294
+ .catch(() => null);
295
+ manifest = fallback?.extensions?.find((e) => e.id === extId);
296
+ manifest ??= null;
297
+ }
298
+ const componentsResp = await this.api
299
+ .getAll({ adminPath: `/apps/${appId}/configs` }, { query: { type: 'component' } })
300
+ .catch(() => ({ results: [] }));
301
+ const allFunctions = (functionsResp?.results ?? []);
302
+ const allComponents = (componentsResp?.results ?? []);
303
+ const declaredIds = new Set((installedApp?.app?.extensions ?? []).map((e) => e.id));
304
+ let detail;
305
+ if (manifest) {
306
+ const fnsForExtension = allFunctions.filter((f) => functionMatchesExtension(f, extId));
307
+ const compsForExtension = allComponents.filter((c) => componentMatchesExtension(c, extId));
308
+ let localDiff = null;
309
+ if (flags.app === '.' && hasAppContext()) {
310
+ const local = await readLocalManifest();
311
+ const localEntry = local?.extensions?.find((e) => e.id === extId);
312
+ if (localEntry) {
313
+ localDiff = diffManifestEntries(localEntry, manifest);
314
+ }
315
+ }
316
+ detail = buildExtensionDetail({
317
+ appId,
318
+ appSlug,
319
+ extId,
320
+ manifest,
321
+ payments,
322
+ shipments,
323
+ taxes,
324
+ functions: fnsForExtension,
325
+ components: compsForExtension,
326
+ localDiff,
327
+ });
328
+ }
329
+ else {
330
+ // Possible orphan: function/component exists with this `extension` value
331
+ // but no manifest entry declares it. Surface as the orphan envelope only
332
+ // when we actually find handlers; otherwise it's a "not found" lookup.
333
+ const orphanFns = allFunctions.filter((f) => f.extension === extId);
334
+ const orphanComps = allComponents.filter((c) => c.values?.extension === extId);
335
+ if (orphanFns.length === 0 && orphanComps.length === 0) {
336
+ this.error(`No extension '${extId}' declared by app '${appSlug}', and no handlers reference it. ` +
337
+ `List available: swell inspect extensions${scope.appSlug ? ` --app=${scope.appSlug}` : ''}`, { exit: 1 });
338
+ }
339
+ // Local-not-deployed: under --app=. with a local manifest entry that
340
+ // hasn't been deployed yet, surface `not deployed` rather than orphan.
341
+ let localEntry = null;
342
+ if (flags.app === '.' && hasAppContext()) {
343
+ const local = await readLocalManifest();
344
+ localEntry = local?.extensions?.find((e) => e.id === extId) ?? null;
345
+ }
346
+ detail =
347
+ localEntry && !declaredIds.has(extId)
348
+ ? buildExtensionDetail({
349
+ appId,
350
+ appSlug,
351
+ extId,
352
+ manifest: localEntry,
353
+ notDeployed: true,
354
+ payments,
355
+ shipments,
356
+ taxes,
357
+ functions: orphanFns,
358
+ components: orphanComps,
359
+ })
360
+ : buildOrphanDetail({
361
+ appId,
362
+ appSlug,
363
+ unresolvedId: extId,
364
+ functions: orphanFns,
365
+ components: orphanComps,
366
+ });
367
+ }
368
+ this.emitDetail(detail, flags);
369
+ }
370
+ /**
371
+ * `extensions` emits a synthesized envelope rather than a raw API record —
372
+ * other inspect subcommands print one upstream record, but here the JSON
373
+ * nests `manifest`, `native_bindings[].record`, `bound.functions[]`, and
374
+ * `bound.components[]` under shared status fields.
375
+ *
376
+ * The `Next steps:` footer mixes runnable shell commands and merchant-UI
377
+ * lines (prefixed `(merchant)`). The structured `action` and `action_owner`
378
+ * fields above give an agent a parseable signal independent of the footer.
379
+ */
380
+ emitDetail(detail, flags) {
381
+ this.log(JSON.stringify(detail, null, 2));
382
+ if (flags.json) {
383
+ return;
384
+ }
385
+ const lines = nextStepLines(detail);
386
+ if (lines.length === 0) {
387
+ return;
388
+ }
389
+ this.log();
390
+ this.log('Next steps:');
391
+ for (const line of lines) {
392
+ this.log(` ${line}`);
393
+ }
394
+ this.log();
395
+ }
396
+ async fetchSafe(adminPath) {
397
+ try {
398
+ const result = await this.api.get({ adminPath });
399
+ return (result ?? null);
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ printPreamble(live) {
406
+ const store = localConfig.getDefaultStore();
407
+ const envLabel = live ? '[live]' : '[test]';
408
+ this.log(`Extensions in '${store}' ${envLabel}`);
409
+ }
410
+ }
411
+ function slugFromInstalled(a) {
412
+ return a.app_private_id?.replace(/^_/, '') || a.app_public_id;
413
+ }
414
+ async function readLocalManifest() {
415
+ if (!hasAppContext())
416
+ return null;
417
+ try {
418
+ const raw = await fs.readFile('swell.json', 'utf8');
419
+ return JSON.parse(raw);
420
+ }
421
+ catch {
422
+ return null;
423
+ }
424
+ }
@@ -175,7 +175,7 @@ export default class Logs extends SwellCommand {
175
175
  type: Flags.string({
176
176
  description: 'filter logs by type',
177
177
  multiple: true,
178
- options: ['api', 'function', 'webhook'],
178
+ options: ['api', 'function', 'webhook', 'transaction'],
179
179
  }),
180
180
  };
181
181
  static summary = 'Output or stream store logs to the terminal.';
@@ -310,9 +310,13 @@ export class CreateAppCommand extends SwellCommand {
310
310
  ); */
311
311
  }
312
312
  catch (error) {
313
- spinner.fail(`Error creating ${projectType?.name} app:\n`);
314
- this.log(error.stdout);
315
- this.log();
313
+ spinner.fail(`Error creating ${projectType?.name} app:`);
314
+ const detail = [error.stdout, error.stderr]
315
+ .map((s) => (s || '').trim())
316
+ .filter(Boolean)
317
+ .join('\n') || (error.message ?? '').trim();
318
+ if (detail)
319
+ this.log(detail);
316
320
  return false;
317
321
  }
318
322
  // Ensure frontend package.json has correct name for workspace
@@ -383,9 +387,13 @@ export class CreateAppCommand extends SwellCommand {
383
387
  }
384
388
  }
385
389
  catch (error) {
386
- spinner.fail(`Error creating theme template:\n`);
387
- this.log(error.stdout);
388
- this.log();
390
+ spinner.fail(`Error creating theme template:`);
391
+ const detail = [error.stdout, error.stderr]
392
+ .map((s) => (s || '').trim())
393
+ .filter(Boolean)
394
+ .join('\n') || (error.message ?? '').trim();
395
+ if (detail)
396
+ this.log(detail);
389
397
  return false;
390
398
  }
391
399
  spinner.succeed(`Theme template initialized in ${configPath}/theme/`);
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { ConfigType } from './index.js';
3
4
  export declare class IgnoringFileError extends Error {
4
5
  constructor(message: string);
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { AppConfig } from './app-config.js';
3
4
  export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js';
4
5
  export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, } from './paths.js';