@ductape/cli 0.2.19 → 0.2.20
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/apply.js +44 -3
- package/dist/commands/products.d.ts +6 -0
- package/dist/commands/products.js +85 -0
- package/dist/commands/resources.js +19 -1
- package/dist/index.js +17 -1
- package/package.json +1 -1
- package/src/commands/apply.ts +51 -3
- package/src/commands/products.ts +95 -0
- package/src/commands/resources.ts +23 -1
- package/src/index.ts +20 -1
- package/test/apply-loaders.test.ts +40 -0
package/dist/commands/apply.js
CHANGED
|
@@ -13,20 +13,62 @@ async function syncItems(type, items, productTag, proxy, dryRun) {
|
|
|
13
13
|
const listResult = await proxy.execute(module, 'list', buildCrudParams(module, 'list', productTag, {}));
|
|
14
14
|
const existingList = Array.isArray(listResult) ? listResult : [];
|
|
15
15
|
const existingTags = new Set(existingList.map((r) => r.tag ?? '').filter(Boolean));
|
|
16
|
+
const existingNotificationMessages = new Set();
|
|
17
|
+
if (type === 'notifications') {
|
|
18
|
+
for (const notification of existingList) {
|
|
19
|
+
const record = notification;
|
|
20
|
+
for (const message of record.messages ?? []) {
|
|
21
|
+
if (record.tag && message.tag) {
|
|
22
|
+
existingNotificationMessages.add(message.tag.includes(':') ? message.tag : `${record.tag}:${message.tag}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
16
27
|
for (const item of items) {
|
|
17
28
|
if (!item.tag) {
|
|
18
29
|
console.warn(` [${type}] skipped — missing "tag" field`);
|
|
19
30
|
continue;
|
|
20
31
|
}
|
|
21
32
|
const verb = existingTags.has(item.tag) ? 'update' : 'create';
|
|
33
|
+
const nestedMessages = type === 'notifications' && Array.isArray(item.messages)
|
|
34
|
+
? item.messages
|
|
35
|
+
: [];
|
|
36
|
+
const componentBody = type === 'notifications'
|
|
37
|
+
? Object.fromEntries(Object.entries(item).filter(([key]) => key !== 'messages'))
|
|
38
|
+
: item;
|
|
22
39
|
if (dryRun) {
|
|
23
40
|
console.log(` [${type}] would ${verb}: ${item.tag}`);
|
|
41
|
+
for (const message of nestedMessages) {
|
|
42
|
+
const rawTag = String(message.tag ?? '');
|
|
43
|
+
const fullTag = rawTag.includes(':') ? rawTag : `${item.tag}:${rawTag}`;
|
|
44
|
+
const messageVerb = existingNotificationMessages.has(fullTag) ? 'update' : 'create';
|
|
45
|
+
console.log(` [notifications] would ${messageVerb} message: ${fullTag}`);
|
|
46
|
+
}
|
|
24
47
|
continue;
|
|
25
48
|
}
|
|
26
49
|
try {
|
|
27
|
-
const params = buildCrudParams(module, verb, productTag, {
|
|
50
|
+
const params = buildCrudParams(module, verb, productTag, {
|
|
51
|
+
tag: item.tag,
|
|
52
|
+
body: componentBody,
|
|
53
|
+
});
|
|
28
54
|
await proxy.execute(module, verb, params);
|
|
29
55
|
console.log(` [${type}] ${verb}d: ${item.tag}`);
|
|
56
|
+
for (const message of nestedMessages) {
|
|
57
|
+
const rawTag = String(message.tag ?? '');
|
|
58
|
+
if (!rawTag)
|
|
59
|
+
throw new Error(`notification "${item.tag}" contains a message without "tag"`);
|
|
60
|
+
const fullTag = rawTag.includes(':') ? rawTag : `${item.tag}:${rawTag}`;
|
|
61
|
+
if (!fullTag.startsWith(`${item.tag}:`)) {
|
|
62
|
+
throw new Error(`message tag "${fullTag}" must belong to notification "${item.tag}"`);
|
|
63
|
+
}
|
|
64
|
+
const messageBody = { ...message, tag: fullTag };
|
|
65
|
+
const messageVerb = existingNotificationMessages.has(fullTag) ? 'update' : 'create';
|
|
66
|
+
const messageParams = messageVerb === 'create'
|
|
67
|
+
? [productTag, messageBody]
|
|
68
|
+
: [productTag, fullTag, messageBody];
|
|
69
|
+
await proxy.execute('notifications', `messages.${messageVerb}`, messageParams);
|
|
70
|
+
console.log(` [notifications] ${messageVerb}d message: ${fullTag}`);
|
|
71
|
+
}
|
|
30
72
|
}
|
|
31
73
|
catch (err) {
|
|
32
74
|
console.error(` [${type}] ${verb} failed for "${item.tag}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -54,8 +96,7 @@ export async function runApply(type, opts) {
|
|
|
54
96
|
items = loaders[t]();
|
|
55
97
|
}
|
|
56
98
|
catch (err) {
|
|
57
|
-
|
|
58
|
-
continue;
|
|
99
|
+
fail(`[${t}] parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
59
100
|
}
|
|
60
101
|
if (items === null) {
|
|
61
102
|
console.log(`[${t}] no ductape/${t}.json found — skipping`);
|
|
@@ -12,6 +12,12 @@ export declare function runProductApps(verb: string, opts: {
|
|
|
12
12
|
product?: string;
|
|
13
13
|
json?: boolean;
|
|
14
14
|
}, extraArgs: string[]): Promise<void>;
|
|
15
|
+
export declare function runProductComponents(verb: string, opts: {
|
|
16
|
+
profile?: string;
|
|
17
|
+
tag?: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
}): Promise<void>;
|
|
15
21
|
export declare function runProductEnvironments(verb: string, opts: {
|
|
16
22
|
product?: string;
|
|
17
23
|
tag?: string;
|
|
@@ -73,6 +73,91 @@ export async function runProductApps(verb, opts, extraArgs) {
|
|
|
73
73
|
const result = await listProductApps(ctx, productId);
|
|
74
74
|
printJson(result, Boolean(opts.json));
|
|
75
75
|
}
|
|
76
|
+
const COMPONENT_KEYS = {
|
|
77
|
+
apps: ['apps'],
|
|
78
|
+
databases: ['databases'],
|
|
79
|
+
storage: ['storage'],
|
|
80
|
+
caches: ['caches'],
|
|
81
|
+
graphs: ['graphs'],
|
|
82
|
+
vectors: ['vectors', 'vector_databases'],
|
|
83
|
+
notifications: ['notifications'],
|
|
84
|
+
events: ['message_brokers', 'messageBrokers', 'brokers'],
|
|
85
|
+
sessions: ['sessions'],
|
|
86
|
+
features: ['features'],
|
|
87
|
+
jobs: ['jobs'],
|
|
88
|
+
};
|
|
89
|
+
function unwrapProduct(value) {
|
|
90
|
+
if (!value || typeof value !== 'object')
|
|
91
|
+
return {};
|
|
92
|
+
const record = value;
|
|
93
|
+
return record.data && typeof record.data === 'object'
|
|
94
|
+
? record.data
|
|
95
|
+
: record;
|
|
96
|
+
}
|
|
97
|
+
function summarizeComponent(value) {
|
|
98
|
+
const component = (value && typeof value === 'object' ? value : {});
|
|
99
|
+
const summary = {};
|
|
100
|
+
for (const key of ['_id', 'id', 'tag', 'name', 'description', 'type', 'status']) {
|
|
101
|
+
if (component[key] !== undefined)
|
|
102
|
+
summary[key] = component[key];
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(component.envs)) {
|
|
105
|
+
summary.envs = component.envs.map((env) => {
|
|
106
|
+
const item = (env && typeof env === 'object' ? env : {});
|
|
107
|
+
return { slug: item.slug, type: item.type, cloud: item.cloud };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const nestedKey of ['topics', 'messages']) {
|
|
111
|
+
if (Array.isArray(component[nestedKey])) {
|
|
112
|
+
summary[nestedKey] = component[nestedKey].map((nested) => {
|
|
113
|
+
const item = (nested && typeof nested === 'object' ? nested : {});
|
|
114
|
+
return {
|
|
115
|
+
tag: item.tag,
|
|
116
|
+
name: item.name,
|
|
117
|
+
description: item.description,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return summary;
|
|
123
|
+
}
|
|
124
|
+
export async function runProductComponents(verb, opts) {
|
|
125
|
+
const normalizedVerb = verb.toLowerCase();
|
|
126
|
+
if (!['list', 'get'].includes(normalizedVerb)) {
|
|
127
|
+
throw new Error('Product components verb must be: list | get');
|
|
128
|
+
}
|
|
129
|
+
if (!opts.tag)
|
|
130
|
+
throw new Error('--tag <product_tag> is required');
|
|
131
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
132
|
+
const product = unwrapProduct(await getProduct(ctx, { tag: opts.tag }));
|
|
133
|
+
const selectedTypes = normalizedVerb === 'get'
|
|
134
|
+
? [opts.type?.toLowerCase()].filter(Boolean)
|
|
135
|
+
: Object.keys(COMPONENT_KEYS);
|
|
136
|
+
if (normalizedVerb === 'get' && !selectedTypes.length) {
|
|
137
|
+
throw new Error('--type <component_type> is required for get');
|
|
138
|
+
}
|
|
139
|
+
for (const type of selectedTypes) {
|
|
140
|
+
if (!COMPONENT_KEYS[type]) {
|
|
141
|
+
throw new Error(`Unknown component type "${type}". Use: ${Object.keys(COMPONENT_KEYS).join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const components = {};
|
|
145
|
+
for (const type of selectedTypes) {
|
|
146
|
+
const sourceKey = COMPONENT_KEYS[type].find((key) => Array.isArray(product[key]));
|
|
147
|
+
components[type] = sourceKey
|
|
148
|
+
? product[sourceKey].map(summarizeComponent)
|
|
149
|
+
: [];
|
|
150
|
+
}
|
|
151
|
+
const envSource = Array.isArray(product.envs) ? product.envs : [];
|
|
152
|
+
printJson({
|
|
153
|
+
tag: product.tag ?? opts.tag,
|
|
154
|
+
environments: envSource.map((env) => {
|
|
155
|
+
const item = (env && typeof env === 'object' ? env : {});
|
|
156
|
+
return { slug: item.slug, name: item.name ?? item.env_name };
|
|
157
|
+
}),
|
|
158
|
+
components,
|
|
159
|
+
}, Boolean(opts.json));
|
|
160
|
+
}
|
|
76
161
|
export async function runProductEnvironments(verb, opts, extraArgs) {
|
|
77
162
|
const v = verb.toLowerCase();
|
|
78
163
|
const allowed = ['list', 'get', 'fetch'];
|
|
@@ -91,7 +91,25 @@ export async function runNotificationMessageCrud(verb, opts) {
|
|
|
91
91
|
throw new Error('--tag <notification:message> is required');
|
|
92
92
|
params = [product, opts.tag, body ?? {}];
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
try {
|
|
95
|
+
printJson(await proxy.execute('notifications', method, params), Boolean(opts.json));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (crud !== 'list' || !opts.notification)
|
|
99
|
+
throw error;
|
|
100
|
+
// Compatibility fallback for platform versions whose message lookup uses a stale product
|
|
101
|
+
// snapshot while the component inventory already contains the notification.
|
|
102
|
+
const listed = await proxy.execute('notifications', 'list', buildCrudParams('notifications', 'list', product, {}));
|
|
103
|
+
const notifications = Array.isArray(listed)
|
|
104
|
+
? listed
|
|
105
|
+
: Array.isArray(listed?.data)
|
|
106
|
+
? listed.data
|
|
107
|
+
: [];
|
|
108
|
+
const notification = notifications.find((item) => item.tag === opts.notification);
|
|
109
|
+
if (!notification)
|
|
110
|
+
throw error;
|
|
111
|
+
printJson(notification.messages ?? [], Boolean(opts.json));
|
|
112
|
+
}
|
|
95
113
|
}
|
|
96
114
|
export async function runResourceCrud(typeName, verb, opts, extraArgs) {
|
|
97
115
|
const crud = verb.toLowerCase();
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { runSecrets } from './commands/secrets.js';
|
|
|
22
22
|
import { runWorkspacesCurrent, runWorkspacesList, runWorkspacesRefresh, runWorkspacesUse, } from './commands/workspaces.js';
|
|
23
23
|
import { runGeneratePayload, runGenerateSnippet } from './commands/generate.js';
|
|
24
24
|
import { runCompletion } from './commands/completion.js';
|
|
25
|
-
import { runProducts, runProductApps, runProductEnvironments } from './commands/products.js';
|
|
25
|
+
import { runProducts, runProductApps, runProductComponents, runProductEnvironments } from './commands/products.js';
|
|
26
26
|
import { runApps } from './commands/apps.js';
|
|
27
27
|
import { runAppsImport } from './commands/apps-import.js';
|
|
28
28
|
import { runApply } from './commands/apply.js';
|
|
@@ -192,6 +192,22 @@ productApps
|
|
|
192
192
|
.option('--product <id>', 'Product _id')
|
|
193
193
|
.option('--json', 'JSON output')
|
|
194
194
|
.action(wrap((opts) => runProductApps('list', opts, [])));
|
|
195
|
+
const productComponents = products
|
|
196
|
+
.command('components')
|
|
197
|
+
.description('Compact, non-secret product component inventory');
|
|
198
|
+
productComponents
|
|
199
|
+
.command('list')
|
|
200
|
+
.requiredOption('-t, --tag <tag>', 'Product tag')
|
|
201
|
+
.option('--profile <name>')
|
|
202
|
+
.option('--json', 'JSON output')
|
|
203
|
+
.action(wrap((opts) => runProductComponents('list', opts)));
|
|
204
|
+
productComponents
|
|
205
|
+
.command('get')
|
|
206
|
+
.requiredOption('-t, --tag <tag>', 'Product tag')
|
|
207
|
+
.requiredOption('--type <type>', 'Component type, e.g. notifications or events')
|
|
208
|
+
.option('--profile <name>')
|
|
209
|
+
.option('--json', 'JSON output')
|
|
210
|
+
.action(wrap((opts) => runProductComponents('get', opts)));
|
|
195
211
|
const productEnvironments = products
|
|
196
212
|
.command('environments')
|
|
197
213
|
.description('Product environments (list, get)');
|
package/package.json
CHANGED
package/src/commands/apply.ts
CHANGED
|
@@ -36,6 +36,19 @@ async function syncItems(
|
|
|
36
36
|
const existingTags = new Set(
|
|
37
37
|
existingList.map((r) => (r as { tag?: string }).tag ?? '').filter(Boolean),
|
|
38
38
|
);
|
|
39
|
+
const existingNotificationMessages = new Set<string>();
|
|
40
|
+
if (type === 'notifications') {
|
|
41
|
+
for (const notification of existingList) {
|
|
42
|
+
const record = notification as { tag?: string; messages?: Array<{ tag?: string }> };
|
|
43
|
+
for (const message of record.messages ?? []) {
|
|
44
|
+
if (record.tag && message.tag) {
|
|
45
|
+
existingNotificationMessages.add(
|
|
46
|
+
message.tag.includes(':') ? message.tag : `${record.tag}:${message.tag}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
39
52
|
|
|
40
53
|
for (const item of items) {
|
|
41
54
|
if (!item.tag) {
|
|
@@ -44,16 +57,52 @@ async function syncItems(
|
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
const verb: 'create' | 'update' = existingTags.has(item.tag) ? 'update' : 'create';
|
|
60
|
+
const nestedMessages =
|
|
61
|
+
type === 'notifications' && Array.isArray(item.messages)
|
|
62
|
+
? item.messages as Array<Record<string, unknown>>
|
|
63
|
+
: [];
|
|
64
|
+
const componentBody =
|
|
65
|
+
type === 'notifications'
|
|
66
|
+
? Object.fromEntries(Object.entries(item).filter(([key]) => key !== 'messages'))
|
|
67
|
+
: item;
|
|
47
68
|
|
|
48
69
|
if (dryRun) {
|
|
49
70
|
console.log(` [${type}] would ${verb}: ${item.tag}`);
|
|
71
|
+
for (const message of nestedMessages) {
|
|
72
|
+
const rawTag = String(message.tag ?? '');
|
|
73
|
+
const fullTag = rawTag.includes(':') ? rawTag : `${item.tag}:${rawTag}`;
|
|
74
|
+
const messageVerb = existingNotificationMessages.has(fullTag) ? 'update' : 'create';
|
|
75
|
+
console.log(` [notifications] would ${messageVerb} message: ${fullTag}`);
|
|
76
|
+
}
|
|
50
77
|
continue;
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
try {
|
|
54
|
-
const params = buildCrudParams(module, verb, productTag, {
|
|
81
|
+
const params = buildCrudParams(module, verb, productTag, {
|
|
82
|
+
tag: item.tag,
|
|
83
|
+
body: componentBody,
|
|
84
|
+
});
|
|
55
85
|
await proxy.execute(module, verb, params);
|
|
56
86
|
console.log(` [${type}] ${verb}d: ${item.tag}`);
|
|
87
|
+
|
|
88
|
+
for (const message of nestedMessages) {
|
|
89
|
+
const rawTag = String(message.tag ?? '');
|
|
90
|
+
if (!rawTag) throw new Error(`notification "${item.tag}" contains a message without "tag"`);
|
|
91
|
+
const fullTag = rawTag.includes(':') ? rawTag : `${item.tag}:${rawTag}`;
|
|
92
|
+
if (!fullTag.startsWith(`${item.tag}:`)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`message tag "${fullTag}" must belong to notification "${item.tag}"`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const messageBody = { ...message, tag: fullTag };
|
|
98
|
+
const messageVerb = existingNotificationMessages.has(fullTag) ? 'update' : 'create';
|
|
99
|
+
const messageParams =
|
|
100
|
+
messageVerb === 'create'
|
|
101
|
+
? [productTag, messageBody]
|
|
102
|
+
: [productTag, fullTag, messageBody];
|
|
103
|
+
await proxy.execute('notifications', `messages.${messageVerb}`, messageParams);
|
|
104
|
+
console.log(` [notifications] ${messageVerb}d message: ${fullTag}`);
|
|
105
|
+
}
|
|
57
106
|
} catch (err) {
|
|
58
107
|
console.error(
|
|
59
108
|
` [${type}] ${verb} failed for "${item.tag}": ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -85,8 +134,7 @@ export async function runApply(type: ApplyType | undefined, opts: ApplyOpts): Pr
|
|
|
85
134
|
try {
|
|
86
135
|
items = loaders[t]();
|
|
87
136
|
} catch (err) {
|
|
88
|
-
|
|
89
|
-
continue;
|
|
137
|
+
fail(`[${t}] parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
138
|
}
|
|
91
139
|
|
|
92
140
|
if (items === null) {
|
package/src/commands/products.ts
CHANGED
|
@@ -104,6 +104,101 @@ export async function runProductApps(
|
|
|
104
104
|
printJson(result, Boolean(opts.json));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
const COMPONENT_KEYS: Record<string, string[]> = {
|
|
108
|
+
apps: ['apps'],
|
|
109
|
+
databases: ['databases'],
|
|
110
|
+
storage: ['storage'],
|
|
111
|
+
caches: ['caches'],
|
|
112
|
+
graphs: ['graphs'],
|
|
113
|
+
vectors: ['vectors', 'vector_databases'],
|
|
114
|
+
notifications: ['notifications'],
|
|
115
|
+
events: ['message_brokers', 'messageBrokers', 'brokers'],
|
|
116
|
+
sessions: ['sessions'],
|
|
117
|
+
features: ['features'],
|
|
118
|
+
jobs: ['jobs'],
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function unwrapProduct(value: unknown): Record<string, unknown> {
|
|
122
|
+
if (!value || typeof value !== 'object') return {};
|
|
123
|
+
const record = value as Record<string, unknown>;
|
|
124
|
+
return record.data && typeof record.data === 'object'
|
|
125
|
+
? (record.data as Record<string, unknown>)
|
|
126
|
+
: record;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function summarizeComponent(value: unknown): Record<string, unknown> {
|
|
130
|
+
const component = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>;
|
|
131
|
+
const summary: Record<string, unknown> = {};
|
|
132
|
+
for (const key of ['_id', 'id', 'tag', 'name', 'description', 'type', 'status']) {
|
|
133
|
+
if (component[key] !== undefined) summary[key] = component[key];
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(component.envs)) {
|
|
136
|
+
summary.envs = component.envs.map((env) => {
|
|
137
|
+
const item = (env && typeof env === 'object' ? env : {}) as Record<string, unknown>;
|
|
138
|
+
return { slug: item.slug, type: item.type, cloud: item.cloud };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const nestedKey of ['topics', 'messages']) {
|
|
142
|
+
if (Array.isArray(component[nestedKey])) {
|
|
143
|
+
summary[nestedKey] = (component[nestedKey] as unknown[]).map((nested) => {
|
|
144
|
+
const item = (nested && typeof nested === 'object' ? nested : {}) as Record<string, unknown>;
|
|
145
|
+
return {
|
|
146
|
+
tag: item.tag,
|
|
147
|
+
name: item.name,
|
|
148
|
+
description: item.description,
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return summary;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function runProductComponents(
|
|
157
|
+
verb: string,
|
|
158
|
+
opts: { profile?: string; tag?: string; type?: string; json?: boolean },
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const normalizedVerb = verb.toLowerCase();
|
|
161
|
+
if (!['list', 'get'].includes(normalizedVerb)) {
|
|
162
|
+
throw new Error('Product components verb must be: list | get');
|
|
163
|
+
}
|
|
164
|
+
if (!opts.tag) throw new Error('--tag <product_tag> is required');
|
|
165
|
+
|
|
166
|
+
const ctx = requireWorkspaceContext({ profile: opts.profile });
|
|
167
|
+
const product = unwrapProduct(await getProduct(ctx, { tag: opts.tag }));
|
|
168
|
+
const selectedTypes =
|
|
169
|
+
normalizedVerb === 'get'
|
|
170
|
+
? [opts.type?.toLowerCase()].filter(Boolean) as string[]
|
|
171
|
+
: Object.keys(COMPONENT_KEYS);
|
|
172
|
+
if (normalizedVerb === 'get' && !selectedTypes.length) {
|
|
173
|
+
throw new Error('--type <component_type> is required for get');
|
|
174
|
+
}
|
|
175
|
+
for (const type of selectedTypes) {
|
|
176
|
+
if (!COMPONENT_KEYS[type]) {
|
|
177
|
+
throw new Error(`Unknown component type "${type}". Use: ${Object.keys(COMPONENT_KEYS).join(', ')}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const components: Record<string, unknown[]> = {};
|
|
182
|
+
for (const type of selectedTypes) {
|
|
183
|
+
const sourceKey = COMPONENT_KEYS[type].find((key) => Array.isArray(product[key]));
|
|
184
|
+
components[type] = sourceKey
|
|
185
|
+
? (product[sourceKey] as unknown[]).map(summarizeComponent)
|
|
186
|
+
: [];
|
|
187
|
+
}
|
|
188
|
+
const envSource = Array.isArray(product.envs) ? product.envs : [];
|
|
189
|
+
printJson(
|
|
190
|
+
{
|
|
191
|
+
tag: product.tag ?? opts.tag,
|
|
192
|
+
environments: envSource.map((env) => {
|
|
193
|
+
const item = (env && typeof env === 'object' ? env : {}) as Record<string, unknown>;
|
|
194
|
+
return { slug: item.slug, name: item.name ?? item.env_name };
|
|
195
|
+
}),
|
|
196
|
+
components,
|
|
197
|
+
},
|
|
198
|
+
Boolean(opts.json),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
107
202
|
export async function runProductEnvironments(
|
|
108
203
|
verb: string,
|
|
109
204
|
opts: { product?: string; tag?: string; slug?: string; json?: boolean },
|
|
@@ -100,7 +100,29 @@ export async function runNotificationMessageCrud(
|
|
|
100
100
|
if (!opts.tag) throw new Error('--tag <notification:message> is required');
|
|
101
101
|
params = [product, opts.tag, body ?? {}];
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
try {
|
|
104
|
+
printJson(await proxy.execute('notifications', method, params), Boolean(opts.json));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (crud !== 'list' || !opts.notification) throw error;
|
|
107
|
+
|
|
108
|
+
// Compatibility fallback for platform versions whose message lookup uses a stale product
|
|
109
|
+
// snapshot while the component inventory already contains the notification.
|
|
110
|
+
const listed = await proxy.execute<unknown>(
|
|
111
|
+
'notifications',
|
|
112
|
+
'list',
|
|
113
|
+
buildCrudParams('notifications', 'list', product, {}),
|
|
114
|
+
);
|
|
115
|
+
const notifications = Array.isArray(listed)
|
|
116
|
+
? listed
|
|
117
|
+
: Array.isArray((listed as { data?: unknown[] } | null)?.data)
|
|
118
|
+
? (listed as { data: unknown[] }).data
|
|
119
|
+
: [];
|
|
120
|
+
const notification = notifications.find(
|
|
121
|
+
(item) => (item as { tag?: string }).tag === opts.notification,
|
|
122
|
+
) as { messages?: unknown[] } | undefined;
|
|
123
|
+
if (!notification) throw error;
|
|
124
|
+
printJson(notification.messages ?? [], Boolean(opts.json));
|
|
125
|
+
}
|
|
104
126
|
}
|
|
105
127
|
|
|
106
128
|
export async function runResourceCrud(
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from './commands/workspaces.js';
|
|
28
28
|
import { runGeneratePayload, runGenerateSnippet } from './commands/generate.js';
|
|
29
29
|
import { runCompletion } from './commands/completion.js';
|
|
30
|
-
import { runProducts, runProductApps, runProductEnvironments } from './commands/products.js';
|
|
30
|
+
import { runProducts, runProductApps, runProductComponents, runProductEnvironments } from './commands/products.js';
|
|
31
31
|
import { runApps } from './commands/apps.js';
|
|
32
32
|
import { runAppsImport } from './commands/apps-import.js';
|
|
33
33
|
import { runApply, type ApplyType } from './commands/apply.js';
|
|
@@ -248,6 +248,25 @@ productApps
|
|
|
248
248
|
.option('--json', 'JSON output')
|
|
249
249
|
.action(wrap((opts) => runProductApps('list', opts, [])));
|
|
250
250
|
|
|
251
|
+
const productComponents = products
|
|
252
|
+
.command('components')
|
|
253
|
+
.description('Compact, non-secret product component inventory');
|
|
254
|
+
|
|
255
|
+
productComponents
|
|
256
|
+
.command('list')
|
|
257
|
+
.requiredOption('-t, --tag <tag>', 'Product tag')
|
|
258
|
+
.option('--profile <name>')
|
|
259
|
+
.option('--json', 'JSON output')
|
|
260
|
+
.action(wrap((opts) => runProductComponents('list', opts)));
|
|
261
|
+
|
|
262
|
+
productComponents
|
|
263
|
+
.command('get')
|
|
264
|
+
.requiredOption('-t, --tag <tag>', 'Product tag')
|
|
265
|
+
.requiredOption('--type <type>', 'Component type, e.g. notifications or events')
|
|
266
|
+
.option('--profile <name>')
|
|
267
|
+
.option('--json', 'JSON output')
|
|
268
|
+
.action(wrap((opts) => runProductComponents('get', opts)));
|
|
269
|
+
|
|
251
270
|
const productEnvironments = products
|
|
252
271
|
.command('environments')
|
|
253
272
|
.description('Product environments (list, get)');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { loadNotifications } from '../src/lib/apply-loaders.js';
|
|
7
|
+
|
|
8
|
+
test('notifications declaration must be a top-level array', () => {
|
|
9
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ductape-notifications-'));
|
|
10
|
+
const configDir = path.join(dir, 'ductape');
|
|
11
|
+
fs.mkdirSync(configDir);
|
|
12
|
+
fs.writeFileSync(
|
|
13
|
+
path.join(configDir, 'notifications.json'),
|
|
14
|
+
JSON.stringify({ notifications: [], messages: [] }),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
assert.throws(
|
|
18
|
+
() => loadNotifications(dir),
|
|
19
|
+
/notifications\.json must be a JSON array/,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('notifications declaration accepts nested messages', () => {
|
|
24
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ductape-notifications-'));
|
|
25
|
+
const configDir = path.join(dir, 'ductape');
|
|
26
|
+
fs.mkdirSync(configDir);
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(configDir, 'notifications.json'),
|
|
29
|
+
JSON.stringify([
|
|
30
|
+
{
|
|
31
|
+
tag: 'game-alerts-critical',
|
|
32
|
+
messages: [{ tag: 'game-alerts-critical:match-launch', name: 'Match launch' }],
|
|
33
|
+
},
|
|
34
|
+
]),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const result = loadNotifications(dir);
|
|
38
|
+
assert.equal(result?.length, 1);
|
|
39
|
+
assert.equal(result?.[0].tag, 'game-alerts-critical');
|
|
40
|
+
});
|