@swell/cli 2.6.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/app/frontend/dev.js +18 -0
- package/dist/commands/inspect/content.d.ts +25 -14
- package/dist/commands/inspect/content.js +34 -144
- package/dist/commands/inspect/extensions.d.ts +49 -0
- package/dist/commands/inspect/extensions.js +424 -0
- package/dist/commands/inspect/functions.d.ts +48 -0
- package/dist/commands/inspect/functions.js +83 -0
- package/dist/commands/inspect/index.js +8 -7
- package/dist/commands/inspect/models.d.ts +17 -2
- package/dist/commands/inspect/models.js +114 -47
- package/dist/commands/inspect/notifications.d.ts +31 -0
- package/dist/commands/inspect/notifications.js +125 -0
- package/dist/commands/inspect/settings.d.ts +28 -0
- package/dist/commands/inspect/settings.js +82 -0
- package/dist/commands/inspect/webhooks.d.ts +34 -0
- package/dist/commands/inspect/webhooks.js +61 -0
- package/dist/create-app-command.d.ts +7 -0
- package/dist/create-app-command.js +80 -9
- package/dist/inspect-resource-command.d.ts +91 -0
- package/dist/inspect-resource-command.js +232 -0
- package/dist/lib/apps/index.d.ts +2 -1
- package/dist/lib/apps/index.js +43 -6
- package/dist/lib/apps/inspect-scope.d.ts +58 -0
- package/dist/lib/apps/inspect-scope.js +60 -0
- package/dist/lib/apps/object-id.d.ts +7 -0
- package/dist/lib/apps/object-id.js +9 -0
- package/dist/lib/apps/paths.js +8 -1
- package/dist/lib/apps/resolve.d.ts +16 -0
- package/dist/lib/apps/resolve.js +39 -0
- package/dist/lib/apps/slug.d.ts +29 -0
- package/dist/lib/apps/slug.js +12 -0
- package/dist/lib/inspect/content.d.ts +39 -0
- package/dist/lib/inspect/content.js +76 -0
- package/dist/lib/inspect/extensions.d.ts +267 -0
- package/dist/lib/inspect/extensions.js +690 -0
- package/dist/lib/inspect/notifications.d.ts +115 -0
- package/dist/lib/inspect/notifications.js +173 -0
- package/dist/lib/inspect/settings.d.ts +61 -0
- package/dist/lib/inspect/settings.js +56 -0
- package/dist/lib/inspect/table.d.ts +29 -0
- package/dist/lib/inspect/table.js +61 -0
- package/dist/push-app-command.js +3 -2
- package/dist/swell-api-command.d.ts +0 -4
- package/dist/swell-api-command.js +2 -18
- package/oclif.manifest.json +392 -35
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { GroupInfo } from './table.js';
|
|
2
|
+
export interface NotificationRecord {
|
|
3
|
+
api?: string;
|
|
4
|
+
app_id?: string;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
id?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
v2?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export type ParsedNotificationKey = {
|
|
12
|
+
id: string;
|
|
13
|
+
kind: 'hex';
|
|
14
|
+
} | {
|
|
15
|
+
id: string;
|
|
16
|
+
kind: 'system_id';
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'app_full';
|
|
19
|
+
model: string;
|
|
20
|
+
name: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'app_short';
|
|
24
|
+
name: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'bare';
|
|
28
|
+
name: string;
|
|
29
|
+
} | {
|
|
30
|
+
input: string;
|
|
31
|
+
kind: 'invalid';
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Translate a stored model path into its display form.
|
|
35
|
+
*
|
|
36
|
+
* apps/<own-hex>/<x> → <x> (own-app prefix is implicit in the slug)
|
|
37
|
+
* apps/<other-hex>/<x> → apps/<other-slug>/<x>
|
|
38
|
+
* apps/<unknown-hex>/<x> → unchanged (preserve a paste-back form)
|
|
39
|
+
* <built-in-model> → unchanged
|
|
40
|
+
*
|
|
41
|
+
* Stripping `apps/<own>/` keeps the common case readable; the rare collision
|
|
42
|
+
* with a same-named built-in (e.g. an app model literally named
|
|
43
|
+
* `subscriptions`) is recovered by `expandModelCandidates` at lookup time.
|
|
44
|
+
*/
|
|
45
|
+
export declare function normalizeModelForDisplay(model: string, ownAppId: string | undefined, appSlugById: Record<string, string>): string;
|
|
46
|
+
/**
|
|
47
|
+
* Produce the column-1 paste-back key for a notification.
|
|
48
|
+
*
|
|
49
|
+
* System (no app_id): record.id verbatim — already in `com.<model>.<name>` form.
|
|
50
|
+
* App with model+name: `app.<slug>.<model-display>.<name>`
|
|
51
|
+
* Anything else: record.id (degenerate, no meaningful slug)
|
|
52
|
+
*
|
|
53
|
+
* Notification identity is `(api, model, name, app_id?)`; collapsing on
|
|
54
|
+
* `(app_id, name)` alone caused two `notify_me.back-in-stock` rows
|
|
55
|
+
* (different `model`) to render identically.
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatNotificationKey(record: NotificationRecord, appSlugById: Record<string, string>): string;
|
|
58
|
+
/**
|
|
59
|
+
* Group a notification record. System rows (no app_id) are not store data —
|
|
60
|
+
* they belong to the platform and render under a `system` divider at the top.
|
|
61
|
+
*/
|
|
62
|
+
export declare function groupNotificationRecord(record: NotificationRecord, appSlugById: Record<string, string>): GroupInfo;
|
|
63
|
+
/**
|
|
64
|
+
* Classify a notification identifier. The base `classifyIdentifier` collapses
|
|
65
|
+
* every dot-segment after `app.<part>.` into `name`, which loses the model.
|
|
66
|
+
* Notifications need a model-aware split, so they bypass the base classifier.
|
|
67
|
+
*
|
|
68
|
+
* Recognised shapes:
|
|
69
|
+
* - 24-char hex → hex (delegate to base GET-by-id)
|
|
70
|
+
* - com.<rest> → system_id (canonical platform id)
|
|
71
|
+
* - app.<slug>.<model>.<name> → app_full (triplet lookup)
|
|
72
|
+
* - app.<slug>.<name> → app_short (3-part legacy; ambiguous)
|
|
73
|
+
* - bare <name> → bare (requires --app= scope; ambiguous)
|
|
74
|
+
* - anything else → invalid
|
|
75
|
+
*
|
|
76
|
+
* Multi-segment names (e.g. `unpaid.v2`) only appear in system notifications,
|
|
77
|
+
* which arrive via the `com.*` form and never go through `app.*` parsing.
|
|
78
|
+
*/
|
|
79
|
+
export declare function parseNotificationKey(input: string): ParsedNotificationKey;
|
|
80
|
+
/**
|
|
81
|
+
* Render the disambiguation error body when a `(app_id, name)` query returns
|
|
82
|
+
* multiple notifications. Caller fetches with `limit = displayLimit + 1` so
|
|
83
|
+
* we can detect overflow without paging through the whole result set; if
|
|
84
|
+
* `results.length > displayLimit`, the surplus rows are dropped and a hint
|
|
85
|
+
* line is appended directing the user to the full key form.
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatDisambiguationError(results: NotificationRecord[], appSlugById: Record<string, string>, identifier: string, displayLimit?: number): string;
|
|
88
|
+
/**
|
|
89
|
+
* Build the `template` value used on send records at `/notifications` from a
|
|
90
|
+
* manifest record's fields. Three shapes:
|
|
91
|
+
*
|
|
92
|
+
* System (no app_id): <model>.<name>
|
|
93
|
+
* App on a standard model: app_<app_id>.<model>.<name>
|
|
94
|
+
* App on a custom (apps/<hex>/x) model: app_<app_id>.apps_<hex>_<x>.<name>
|
|
95
|
+
*
|
|
96
|
+
* The custom-model branch transforms slashes to underscores in the model
|
|
97
|
+
* segment.
|
|
98
|
+
*
|
|
99
|
+
* V2 system notifications have `name` literally ending in `.v2` (mirroring the
|
|
100
|
+
* `id` and the `v2: true` flag), but the send-record `template` strips that
|
|
101
|
+
* suffix. App notifications carry `v2: true` without the suffix in `name`, so
|
|
102
|
+
* they pass through unchanged.
|
|
103
|
+
*
|
|
104
|
+
* Returns undefined when `model` or `name` is missing.
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildNotificationTemplate(record: NotificationRecord): string | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Expand a display-form model into the candidate stored values to try at
|
|
109
|
+
* lookup time. A bare display token can mean either:
|
|
110
|
+
* - the literal built-in model (e.g. `subscriptions`)
|
|
111
|
+
* - an own-app model whose `apps/<own>/` prefix was stripped for display
|
|
112
|
+
* so we try both. `apps/<slug>/x` is resolved to `apps/<hex>/x`; an already-hex
|
|
113
|
+
* `apps/<hex>/x` passes through unchanged.
|
|
114
|
+
*/
|
|
115
|
+
export declare function expandModelCandidates(modelDisplay: string, appId: string, resolveAppId: (slug: string) => Promise<string>): Promise<string[]>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { HEX24 } from '../apps/object-id.js';
|
|
2
|
+
const APPS_MODEL = /^apps\/([^/]+)\/(.+)$/;
|
|
3
|
+
/**
|
|
4
|
+
* Translate a stored model path into its display form.
|
|
5
|
+
*
|
|
6
|
+
* apps/<own-hex>/<x> → <x> (own-app prefix is implicit in the slug)
|
|
7
|
+
* apps/<other-hex>/<x> → apps/<other-slug>/<x>
|
|
8
|
+
* apps/<unknown-hex>/<x> → unchanged (preserve a paste-back form)
|
|
9
|
+
* <built-in-model> → unchanged
|
|
10
|
+
*
|
|
11
|
+
* Stripping `apps/<own>/` keeps the common case readable; the rare collision
|
|
12
|
+
* with a same-named built-in (e.g. an app model literally named
|
|
13
|
+
* `subscriptions`) is recovered by `expandModelCandidates` at lookup time.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeModelForDisplay(model, ownAppId, appSlugById) {
|
|
16
|
+
const match = model.match(APPS_MODEL);
|
|
17
|
+
if (!match)
|
|
18
|
+
return model;
|
|
19
|
+
const [, hex, rest] = match;
|
|
20
|
+
if (ownAppId && hex === ownAppId)
|
|
21
|
+
return rest;
|
|
22
|
+
const slug = appSlugById[hex];
|
|
23
|
+
return slug ? `apps/${slug}/${rest}` : model;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Produce the column-1 paste-back key for a notification.
|
|
27
|
+
*
|
|
28
|
+
* System (no app_id): record.id verbatim — already in `com.<model>.<name>` form.
|
|
29
|
+
* App with model+name: `app.<slug>.<model-display>.<name>`
|
|
30
|
+
* Anything else: record.id (degenerate, no meaningful slug)
|
|
31
|
+
*
|
|
32
|
+
* Notification identity is `(api, model, name, app_id?)`; collapsing on
|
|
33
|
+
* `(app_id, name)` alone caused two `notify_me.back-in-stock` rows
|
|
34
|
+
* (different `model`) to render identically.
|
|
35
|
+
*/
|
|
36
|
+
export function formatNotificationKey(record, appSlugById) {
|
|
37
|
+
if (!record.app_id) {
|
|
38
|
+
return record.id ?? '-';
|
|
39
|
+
}
|
|
40
|
+
if (!record.name || !record.model) {
|
|
41
|
+
return record.id ?? '-';
|
|
42
|
+
}
|
|
43
|
+
const slug = appSlugById[record.app_id] ?? record.app_id;
|
|
44
|
+
const modelDisplay = normalizeModelForDisplay(record.model, record.app_id, appSlugById);
|
|
45
|
+
return `app.${slug}.${modelDisplay}.${record.name}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Group a notification record. System rows (no app_id) are not store data —
|
|
49
|
+
* they belong to the platform and render under a `system` divider at the top.
|
|
50
|
+
*/
|
|
51
|
+
export function groupNotificationRecord(record, appSlugById) {
|
|
52
|
+
if (!record.app_id) {
|
|
53
|
+
return { slug: '<system>', label: 'system', order: 0 };
|
|
54
|
+
}
|
|
55
|
+
const resolved = appSlugById[record.app_id];
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
return { slug: '<not resolved>', order: 2 };
|
|
58
|
+
}
|
|
59
|
+
return { slug: resolved };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Classify a notification identifier. The base `classifyIdentifier` collapses
|
|
63
|
+
* every dot-segment after `app.<part>.` into `name`, which loses the model.
|
|
64
|
+
* Notifications need a model-aware split, so they bypass the base classifier.
|
|
65
|
+
*
|
|
66
|
+
* Recognised shapes:
|
|
67
|
+
* - 24-char hex → hex (delegate to base GET-by-id)
|
|
68
|
+
* - com.<rest> → system_id (canonical platform id)
|
|
69
|
+
* - app.<slug>.<model>.<name> → app_full (triplet lookup)
|
|
70
|
+
* - app.<slug>.<name> → app_short (3-part legacy; ambiguous)
|
|
71
|
+
* - bare <name> → bare (requires --app= scope; ambiguous)
|
|
72
|
+
* - anything else → invalid
|
|
73
|
+
*
|
|
74
|
+
* Multi-segment names (e.g. `unpaid.v2`) only appear in system notifications,
|
|
75
|
+
* which arrive via the `com.*` form and never go through `app.*` parsing.
|
|
76
|
+
*/
|
|
77
|
+
export function parseNotificationKey(input) {
|
|
78
|
+
if (HEX24.test(input)) {
|
|
79
|
+
return { kind: 'hex', id: input };
|
|
80
|
+
}
|
|
81
|
+
if (input.startsWith('com.')) {
|
|
82
|
+
return { kind: 'system_id', id: input };
|
|
83
|
+
}
|
|
84
|
+
if (input.startsWith('app.')) {
|
|
85
|
+
const parts = input.split('.');
|
|
86
|
+
// Reject any post-prefix empty segment (`app..foo`, `app.foo.`, etc.).
|
|
87
|
+
// Without this, `app..foo` would classify as `app_short` with an empty
|
|
88
|
+
// slug and dispatch a useless `/client/apps` lookup.
|
|
89
|
+
const hasEmptySegment = parts.some((p, i) => i > 0 && p.length === 0);
|
|
90
|
+
if (hasEmptySegment) {
|
|
91
|
+
return { kind: 'invalid', input };
|
|
92
|
+
}
|
|
93
|
+
if (parts.length === 3) {
|
|
94
|
+
return { kind: 'app_short', slug: parts[1], name: parts[2] };
|
|
95
|
+
}
|
|
96
|
+
if (parts.length >= 4) {
|
|
97
|
+
const slug = parts[1];
|
|
98
|
+
const name = parts.at(-1);
|
|
99
|
+
const model = parts.slice(2, -1).join('.');
|
|
100
|
+
return { kind: 'app_full', slug, model, name };
|
|
101
|
+
}
|
|
102
|
+
return { kind: 'invalid', input };
|
|
103
|
+
}
|
|
104
|
+
if (/^[\w-]+$/.test(input)) {
|
|
105
|
+
return { kind: 'bare', name: input };
|
|
106
|
+
}
|
|
107
|
+
return { kind: 'invalid', input };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Render the disambiguation error body when a `(app_id, name)` query returns
|
|
111
|
+
* multiple notifications. Caller fetches with `limit = displayLimit + 1` so
|
|
112
|
+
* we can detect overflow without paging through the whole result set; if
|
|
113
|
+
* `results.length > displayLimit`, the surplus rows are dropped and a hint
|
|
114
|
+
* line is appended directing the user to the full key form.
|
|
115
|
+
*/
|
|
116
|
+
export function formatDisambiguationError(results, appSlugById, identifier, displayLimit = 10) {
|
|
117
|
+
const truncated = results.length > displayLimit;
|
|
118
|
+
const visible = truncated ? results.slice(0, displayLimit) : results;
|
|
119
|
+
const lines = visible.map((r) => ` ${formatNotificationKey(r, appSlugById)}`);
|
|
120
|
+
if (truncated) {
|
|
121
|
+
lines.push(' … (+more candidates; pass the full key)');
|
|
122
|
+
}
|
|
123
|
+
return `Multiple notifications match '${identifier}'. Use the full key:\n${lines.join('\n')}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build the `template` value used on send records at `/notifications` from a
|
|
127
|
+
* manifest record's fields. Three shapes:
|
|
128
|
+
*
|
|
129
|
+
* System (no app_id): <model>.<name>
|
|
130
|
+
* App on a standard model: app_<app_id>.<model>.<name>
|
|
131
|
+
* App on a custom (apps/<hex>/x) model: app_<app_id>.apps_<hex>_<x>.<name>
|
|
132
|
+
*
|
|
133
|
+
* The custom-model branch transforms slashes to underscores in the model
|
|
134
|
+
* segment.
|
|
135
|
+
*
|
|
136
|
+
* V2 system notifications have `name` literally ending in `.v2` (mirroring the
|
|
137
|
+
* `id` and the `v2: true` flag), but the send-record `template` strips that
|
|
138
|
+
* suffix. App notifications carry `v2: true` without the suffix in `name`, so
|
|
139
|
+
* they pass through unchanged.
|
|
140
|
+
*
|
|
141
|
+
* Returns undefined when `model` or `name` is missing.
|
|
142
|
+
*/
|
|
143
|
+
export function buildNotificationTemplate(record) {
|
|
144
|
+
if (!record.model || !record.name) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const name = record.v2 === true && record.name.endsWith('.v2')
|
|
148
|
+
? record.name.slice(0, -'.v2'.length)
|
|
149
|
+
: record.name;
|
|
150
|
+
const match = record.model.match(APPS_MODEL);
|
|
151
|
+
const modelSegment = match ? `apps_${match[1]}_${match[2]}` : record.model;
|
|
152
|
+
const prefix = record.app_id ? `app_${record.app_id}.` : '';
|
|
153
|
+
return `${prefix}${modelSegment}.${name}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Expand a display-form model into the candidate stored values to try at
|
|
157
|
+
* lookup time. A bare display token can mean either:
|
|
158
|
+
* - the literal built-in model (e.g. `subscriptions`)
|
|
159
|
+
* - an own-app model whose `apps/<own>/` prefix was stripped for display
|
|
160
|
+
* so we try both. `apps/<slug>/x` is resolved to `apps/<hex>/x`; an already-hex
|
|
161
|
+
* `apps/<hex>/x` passes through unchanged.
|
|
162
|
+
*/
|
|
163
|
+
export async function expandModelCandidates(modelDisplay, appId, resolveAppId) {
|
|
164
|
+
const match = modelDisplay.match(APPS_MODEL);
|
|
165
|
+
if (match) {
|
|
166
|
+
const [, sluglike, rest] = match;
|
|
167
|
+
if (HEX24.test(sluglike))
|
|
168
|
+
return [modelDisplay];
|
|
169
|
+
const hex = await resolveAppId(sluglike);
|
|
170
|
+
return [`apps/${hex}/${rest}`];
|
|
171
|
+
}
|
|
172
|
+
return [modelDisplay, `apps/${appId}/${modelDisplay}`];
|
|
173
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { GroupInfo } from './table.js';
|
|
2
|
+
export interface SettingRecord {
|
|
3
|
+
api?: string;
|
|
4
|
+
app_id?: string | null;
|
|
5
|
+
deprecated?: boolean | null;
|
|
6
|
+
fields?: Record<string, unknown>;
|
|
7
|
+
id?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Settings identifier grammar (calibrated against `/data/:settings`).
|
|
13
|
+
*
|
|
14
|
+
* `app.<slug-or-hex>` → kind 'app' — slug must be translated to hex before
|
|
15
|
+
* a path GET; hex passes through.
|
|
16
|
+
* anything else valid → kind 'path' — direct path GET. The endpoint resolves
|
|
17
|
+
* bare system names (`taxes`), bare app
|
|
18
|
+
* hexes, and full dotted ids
|
|
19
|
+
* (`com.taxes`) on the path. No `name`
|
|
20
|
+
* query needed.
|
|
21
|
+
* anything else → kind 'invalid'
|
|
22
|
+
*
|
|
23
|
+
* The 3-segment `app.<slug>.<name>` form is rejected: settings collapse to one
|
|
24
|
+
* record per app at push time, so there is no sub-section identity the API can
|
|
25
|
+
* resolve back. Users wanting a single section run `--json | jq`.
|
|
26
|
+
*/
|
|
27
|
+
export type ParsedSettingsKey = {
|
|
28
|
+
kind: 'app';
|
|
29
|
+
ref: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'path';
|
|
32
|
+
segment: string;
|
|
33
|
+
} | {
|
|
34
|
+
input: string;
|
|
35
|
+
kind: 'invalid';
|
|
36
|
+
};
|
|
37
|
+
export declare function parseSettingsKey(input: string): ParsedSettingsKey;
|
|
38
|
+
/**
|
|
39
|
+
* Produce the column-1 paste-back key for a settings record.
|
|
40
|
+
*
|
|
41
|
+
* System (no app_id): `record.name` (e.g. `taxes`).
|
|
42
|
+
* App with slug: `app.<slug>`.
|
|
43
|
+
* App without slug: `app.<hex>` (still pasteable — `app.<hex>` resolves).
|
|
44
|
+
*
|
|
45
|
+
* App records carry `name === app_id` (the hex), which is meaningless to a
|
|
46
|
+
* human; we never surface it. Ultimate fallback is `record.id` for degenerate
|
|
47
|
+
* records that have neither `app_id` nor `name`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatSettingsKey(record: SettingRecord, appSlugById: Record<string, string>): string;
|
|
50
|
+
/**
|
|
51
|
+
* Group a settings record. System rows (`app_id` null/undefined) belong to the
|
|
52
|
+
* platform and render under a `system` divider at the top, mirroring the
|
|
53
|
+
* notifications shape.
|
|
54
|
+
*/
|
|
55
|
+
export declare function groupSettingsRecord(record: SettingRecord, appSlugById: Record<string, string>): GroupInfo;
|
|
56
|
+
/**
|
|
57
|
+
* `general` and `admin` are flagged `deprecated: true` in the base schema and
|
|
58
|
+
* still come back from the default list (no server-side filter equivalent —
|
|
59
|
+
* `deprecated[$ne]=true` doesn't take). Filter them out client-side.
|
|
60
|
+
*/
|
|
61
|
+
export declare function isDeprecatedRecord(record: SettingRecord): boolean;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const PATH_SEGMENT = /^[\w.-]+$/;
|
|
2
|
+
const APP_PREFIX = /^app\.([^.]+)$/;
|
|
3
|
+
export function parseSettingsKey(input) {
|
|
4
|
+
const appMatch = input.match(APP_PREFIX);
|
|
5
|
+
if (appMatch) {
|
|
6
|
+
return { kind: 'app', ref: appMatch[1] };
|
|
7
|
+
}
|
|
8
|
+
if (input.startsWith('app.')) {
|
|
9
|
+
return { kind: 'invalid', input };
|
|
10
|
+
}
|
|
11
|
+
if (PATH_SEGMENT.test(input)) {
|
|
12
|
+
return { kind: 'path', segment: input };
|
|
13
|
+
}
|
|
14
|
+
return { kind: 'invalid', input };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Produce the column-1 paste-back key for a settings record.
|
|
18
|
+
*
|
|
19
|
+
* System (no app_id): `record.name` (e.g. `taxes`).
|
|
20
|
+
* App with slug: `app.<slug>`.
|
|
21
|
+
* App without slug: `app.<hex>` (still pasteable — `app.<hex>` resolves).
|
|
22
|
+
*
|
|
23
|
+
* App records carry `name === app_id` (the hex), which is meaningless to a
|
|
24
|
+
* human; we never surface it. Ultimate fallback is `record.id` for degenerate
|
|
25
|
+
* records that have neither `app_id` nor `name`.
|
|
26
|
+
*/
|
|
27
|
+
export function formatSettingsKey(record, appSlugById) {
|
|
28
|
+
if (!record.app_id) {
|
|
29
|
+
return record.name ?? record.id ?? '-';
|
|
30
|
+
}
|
|
31
|
+
const slug = appSlugById[record.app_id] ?? record.app_id;
|
|
32
|
+
return `app.${slug}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Group a settings record. System rows (`app_id` null/undefined) belong to the
|
|
36
|
+
* platform and render under a `system` divider at the top, mirroring the
|
|
37
|
+
* notifications shape.
|
|
38
|
+
*/
|
|
39
|
+
export function groupSettingsRecord(record, appSlugById) {
|
|
40
|
+
if (!record.app_id) {
|
|
41
|
+
return { slug: '<system>', label: 'system', order: 0 };
|
|
42
|
+
}
|
|
43
|
+
const resolved = appSlugById[record.app_id];
|
|
44
|
+
if (!resolved) {
|
|
45
|
+
return { slug: '<not resolved>', order: 2 };
|
|
46
|
+
}
|
|
47
|
+
return { slug: resolved };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `general` and `admin` are flagged `deprecated: true` in the base schema and
|
|
51
|
+
* still come back from the default list (no server-side filter equivalent —
|
|
52
|
+
* `deprecated[$ne]=true` doesn't take). Filter them out client-side.
|
|
53
|
+
*/
|
|
54
|
+
export function isDeprecatedRecord(record) {
|
|
55
|
+
return record.deprecated === true;
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface GroupInfo {
|
|
2
|
+
/** Grouping identity. Rows with the same slug land in the same section. */
|
|
3
|
+
slug: string;
|
|
4
|
+
/** Divider text. Defaults to `slug` when omitted. */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Sort priority; lower renders first. Ties preserve insertion order. Default 1. */
|
|
7
|
+
order?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface KeyMetaRow {
|
|
10
|
+
/** Paste-back identifier rendered as column 1. */
|
|
11
|
+
key: string;
|
|
12
|
+
/** Optional compact status string. Empty or undefined renders the key alone. */
|
|
13
|
+
meta?: string;
|
|
14
|
+
/** Optional group; when some rows carry one, multiple groups produce dividers. */
|
|
15
|
+
group?: GroupInfo;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Render a two-column list: paste-back key + optional compact meta string.
|
|
19
|
+
*
|
|
20
|
+
* - Rows with meta render as ` <key-padded> <meta>`.
|
|
21
|
+
* - Rows without meta render as ` <key>` with no trailing whitespace.
|
|
22
|
+
* - Key column width is computed globally across all rows so meta columns
|
|
23
|
+
* align vertically.
|
|
24
|
+
* - Rows with a `group` are collected into sections; sections are separated
|
|
25
|
+
* by a blank line and a `── <label>` divider. Sections sort by `order`
|
|
26
|
+
* (lower first), ties preserve insertion order. A single group collapses
|
|
27
|
+
* to a flat listing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderKeyMetaTable(rows: KeyMetaRow[]): string[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a two-column list: paste-back key + optional compact meta string.
|
|
3
|
+
*
|
|
4
|
+
* - Rows with meta render as ` <key-padded> <meta>`.
|
|
5
|
+
* - Rows without meta render as ` <key>` with no trailing whitespace.
|
|
6
|
+
* - Key column width is computed globally across all rows so meta columns
|
|
7
|
+
* align vertically.
|
|
8
|
+
* - Rows with a `group` are collected into sections; sections are separated
|
|
9
|
+
* by a blank line and a `── <label>` divider. Sections sort by `order`
|
|
10
|
+
* (lower first), ties preserve insertion order. A single group collapses
|
|
11
|
+
* to a flat listing.
|
|
12
|
+
*/
|
|
13
|
+
export function renderKeyMetaTable(rows) {
|
|
14
|
+
if (rows.length === 0) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const keyWidth = Math.max(...rows.map((r) => r.key.length));
|
|
18
|
+
const formatRow = (row) => {
|
|
19
|
+
if (row.meta && row.meta.length > 0) {
|
|
20
|
+
return ` ${row.key.padEnd(keyWidth)} ${row.meta}`;
|
|
21
|
+
}
|
|
22
|
+
return ` ${row.key}`;
|
|
23
|
+
};
|
|
24
|
+
const hasGroups = rows.some((r) => r.group !== undefined);
|
|
25
|
+
if (!hasGroups) {
|
|
26
|
+
return rows.map((row) => formatRow(row));
|
|
27
|
+
}
|
|
28
|
+
const groupMap = new Map();
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
const info = row.group;
|
|
31
|
+
if (!info)
|
|
32
|
+
continue;
|
|
33
|
+
const existing = groupMap.get(info.slug);
|
|
34
|
+
if (existing) {
|
|
35
|
+
existing.rows.push(row);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
groupMap.set(info.slug, {
|
|
39
|
+
slug: info.slug,
|
|
40
|
+
label: info.label ?? info.slug,
|
|
41
|
+
order: info.order ?? 1,
|
|
42
|
+
rows: [row],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (groupMap.size === 1) {
|
|
47
|
+
return rows.map((row) => formatRow(row));
|
|
48
|
+
}
|
|
49
|
+
const ordered = [...groupMap.values()].sort((a, b) => a.order - b.order);
|
|
50
|
+
const lines = [];
|
|
51
|
+
for (const group of ordered) {
|
|
52
|
+
if (lines.length > 0) {
|
|
53
|
+
lines.push('');
|
|
54
|
+
}
|
|
55
|
+
lines.push(`── ${group.label}`);
|
|
56
|
+
for (const row of group.rows) {
|
|
57
|
+
lines.push(formatRow(row));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
package/dist/push-app-command.js
CHANGED
|
@@ -6,8 +6,9 @@ import * as path from 'node:path';
|
|
|
6
6
|
import Stream from 'node:stream';
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import { default as swellConfig } from './lib/app-config.js';
|
|
9
|
-
import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig,
|
|
9
|
+
import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
|
|
10
10
|
import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
|
|
11
|
+
import { slugFromApp } from './lib/apps/slug.js';
|
|
11
12
|
import { default as localConfig } from './lib/config.js';
|
|
12
13
|
import { toAppId } from './lib/create/index.js';
|
|
13
14
|
import { detectPackageManager, transformCommand, } from './lib/package-manager.js';
|
|
@@ -139,7 +140,7 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
139
140
|
}
|
|
140
141
|
const appSlugId = (await select({
|
|
141
142
|
choices: apps.results.map((app) => ({
|
|
142
|
-
name: `${style.appConfigValue(app.name)} (${
|
|
143
|
+
name: `${style.appConfigValue(app.name)} (${slugFromApp(app) ?? app.id})`,
|
|
143
144
|
value: app.private_id,
|
|
144
145
|
})),
|
|
145
146
|
message: `Choose ${typeLabelPrefixed} to pull`,
|
|
@@ -6,10 +6,6 @@ export declare abstract class SwellApiCommand extends SwellCommand {
|
|
|
6
6
|
protected request(command: typeof SwellApiCommand, requestOptions?: Api.RequestOptions): Promise<void>;
|
|
7
7
|
protected catch(error: Error): Promise<any>;
|
|
8
8
|
private parseCommand;
|
|
9
|
-
/**
|
|
10
|
-
* Resolve app ObjectId from a friendly slug or return as-is if already an ObjectId.
|
|
11
|
-
*/
|
|
12
|
-
private resolveAppId;
|
|
13
9
|
/**
|
|
14
10
|
* Resolve function ID from app ID and function name.
|
|
15
11
|
*/
|
|
@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { FetchError } from 'node-fetch';
|
|
5
5
|
import { HttpMethod } from './lib/api.js';
|
|
6
|
+
import { resolveAppId } from './lib/apps/resolve.js';
|
|
6
7
|
import { SwellCommand } from './swell-command.js';
|
|
7
8
|
// Pattern to match /functions/{appId}/{functionName} with optional query string
|
|
8
9
|
const FUNCTION_PATH_REGEX = /^\/functions\/([^/]+)\/([^/?]+)(\?.*)?$/;
|
|
@@ -85,28 +86,11 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
85
86
|
},
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Resolve app ObjectId from a friendly slug or return as-is if already an ObjectId.
|
|
90
|
-
*/
|
|
91
|
-
async resolveAppId(appIdOrSlug) {
|
|
92
|
-
// If it looks like an ObjectId (24 hex chars), return as-is
|
|
93
|
-
if (/^[\da-f]{24}$/i.test(appIdOrSlug)) {
|
|
94
|
-
return appIdOrSlug;
|
|
95
|
-
}
|
|
96
|
-
// Fetch all installed apps and filter by public_id or private_id client-side
|
|
97
|
-
const installedApps = await this.api.get({ adminPath: `/client/apps` });
|
|
98
|
-
const app = installedApps?.results?.find((a) => a.app_public_id === appIdOrSlug ||
|
|
99
|
-
a.app_private_id === `_${appIdOrSlug}`);
|
|
100
|
-
if (!app) {
|
|
101
|
-
throw new Error(`App '${appIdOrSlug}' not found`);
|
|
102
|
-
}
|
|
103
|
-
return app.app_id;
|
|
104
|
-
}
|
|
105
89
|
/**
|
|
106
90
|
* Resolve function ID from app ID and function name.
|
|
107
91
|
*/
|
|
108
92
|
async resolveFunctionId(appIdOrSlug, functionName) {
|
|
109
|
-
const appId = await this.
|
|
93
|
+
const appId = await resolveAppId(this.api, appIdOrSlug);
|
|
110
94
|
const functionRecord = await this.api.get({ adminPath: `/data/:functions` }, {
|
|
111
95
|
query: {
|
|
112
96
|
app_id: appId,
|