@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.
@@ -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, { tag: item.tag, body: item });
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
- console.error(`[${t}] parse error: ${err instanceof Error ? err.message : String(err)}`);
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
- printJson(await proxy.execute('notifications', method, params), Boolean(opts.json));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/cli",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
4
4
  "description": "Ductape CLI — local platform, login, link projects, and manage resources via the proxy (Workbench-compatible)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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, { tag: item.tag, body: item });
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
- console.error(`[${t}] parse error: ${err instanceof Error ? err.message : String(err)}`);
89
- continue;
137
+ fail(`[${t}] parse error: ${err instanceof Error ? err.message : String(err)}`);
90
138
  }
91
139
 
92
140
  if (items === null) {
@@ -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
- printJson(await proxy.execute('notifications', method, params), Boolean(opts.json));
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
+ });