@swell/cli 2.7.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/inspect/extensions.d.ts +49 -0
- package/dist/commands/inspect/extensions.js +424 -0
- package/dist/create-app-command.js +14 -6
- package/dist/lib/inspect/extensions.d.ts +267 -0
- package/dist/lib/inspect/extensions.js +690 -0
- package/oclif.manifest.json +65 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
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/`);
|