@nan0web/ui 1.12.1 → 1.12.3

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.
Files changed (37) hide show
  1. package/README.md +18 -345
  2. package/package.json +13 -8
  3. package/src/Model/index.js +2 -2
  4. package/src/core/GeneratorRunner.js +8 -0
  5. package/src/core/resolvePositionalArgs.js +51 -0
  6. package/src/domain/Content.js +5 -5
  7. package/src/domain/Document.js +1 -1
  8. package/src/domain/HeroModel.js +1 -1
  9. package/src/domain/ModelAsApp.js +310 -20
  10. package/src/domain/ModelAsApp.story.js +117 -0
  11. package/src/domain/app/GalleryCommand.js +9 -8
  12. package/src/domain/app/{GalleryRenderIntent.js → GalleryRenderCommand.js} +20 -20
  13. package/src/domain/app/SnapshotAuditor.js +81 -85
  14. package/src/domain/app/SnapshotRunner.js +1 -1
  15. package/src/domain/app/UIApp.js +12 -21
  16. package/src/index.js +4 -2
  17. package/src/inspect.js +1 -0
  18. package/src/testing/SnapshotRunner.js +2 -1
  19. package/src/testing/SpecRunner.js +37 -0
  20. package/types/Model/index.d.ts +2 -2
  21. package/types/core/resolvePositionalArgs.d.ts +24 -0
  22. package/types/docs/README.md.d.ts +1 -0
  23. package/types/domain/Content.d.ts +2 -2
  24. package/types/domain/Document.d.ts +2 -2
  25. package/types/domain/HeroModel.d.ts +2 -2
  26. package/types/domain/ModelAsApp.d.ts +49 -5
  27. package/types/domain/ModelAsApp.story.d.ts +1 -0
  28. package/types/domain/app/GalleryCommand.d.ts +6 -37
  29. package/types/domain/app/GalleryRenderCommand.d.ts +27 -0
  30. package/types/domain/app/SnapshotAuditor.d.ts +33 -23
  31. package/types/domain/app/SnapshotRunner.d.ts +2 -2
  32. package/types/domain/app/UIApp.d.ts +14 -11
  33. package/types/index.d.ts +4 -2
  34. package/types/inspect.d.ts +1 -0
  35. package/types/testing/SpecRunner.d.ts +22 -0
  36. package/types/testing/verifySnapshot.d.ts +1 -1
  37. package/types/domain/app/GalleryRenderIntent.d.ts +0 -31
@@ -1,46 +1,336 @@
1
- import { Model } from '@nan0web/types'
2
- import { result } from '../core/Intent.js'
1
+ import { Model, getMetadata } from '@nan0web/types'
2
+ import { result, ask, show } from '../core/Intent.js'
3
3
  import { InputAdapter } from '../core/InputAdapter.js'
4
+ import { resolvePositionalArgs } from '../core/resolvePositionalArgs.js'
4
5
 
5
- /** @typedef {import('@nan0web/types').ModelOptions & { adapter: InputAdapter }} ModelAsAppOptions */
6
+ /**
7
+ * @typedef {Object} AppOptions
8
+ * @property {InputAdapter} adapter
9
+ * @property {string} parentPath
10
+ * @property {boolean} _isExplicit
11
+ */
12
+ /** @typedef {import('@nan0web/types').ModelOptions & AppOptions} ModelAsAppOptions */
6
13
 
7
14
  /**
8
15
  * The model with a run generator.
16
+ * @property {boolean} help Show help
9
17
  */
10
18
  export class ModelAsApp extends Model {
11
- /** @type {ModelAsAppOptions} */
12
- #appOptions = {
13
- t: (key) => key,
14
- plugins: {},
15
- adapter: null,
19
+ static help = {
20
+ help: 'Show help',
21
+ default: false,
16
22
  }
23
+
17
24
  /**
18
25
  * @param {Partial<ModelAsApp> | Record<string, any>} [data={}]
19
26
  * @param {Partial<ModelAsAppOptions>} [options={}]
20
27
  */
21
28
  constructor(data = {}, options = {}) {
22
29
  super(data, options)
23
- this.#appOptions = {
24
- ...options,
25
- t:
26
- options.t ||
27
- ((key, props = {}) =>
28
- String(key)
29
- .replace(/{(\w+)}/g, (_, x) => props[x] ?? `{${x}}`)
30
- .replace(/_/g, ' ')),
31
- plugins: options.plugins || {},
30
+ /** @type {boolean} Show help */ this.help
31
+ this._ = {
32
+ ...this._,
32
33
  adapter: options.adapter || new InputAdapter(),
34
+ parentPath: String(options.parentPath || ''),
35
+ _isExplicit: Boolean(options._isExplicit),
36
+ }
37
+
38
+ // ── Automated Sub-Command Instantiation ──
39
+ const metadata = getMetadata(this.constructor)
40
+ for (const [key, meta] of Object.entries(metadata)) {
41
+ if (meta && typeof meta === 'object' && Array.isArray(meta.options)) {
42
+ const val = /** @type {any} */ (this)[key]
43
+ if (val) {
44
+ /** @type {any} */ this[key] = this._instantiateSubCommand(key, val, data)
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Instantiates a subcommand if the value matches one of the options.
52
+ * @param {string} key - Field name.
53
+ * @param {any} val - Current value (string, class, or instance).
54
+ * @param {any} [data={}] - Data to pass to the new instance.
55
+ * @returns {any} Instantiated subcommand or original value.
56
+ */
57
+ _instantiateSubCommand(key, val, data = {}) {
58
+ if (val && typeof val === 'object' && typeof val.run === 'function') return val
59
+
60
+ const Class = /** @type {typeof ModelAsApp} */ (this.constructor)
61
+ const meta = /** @type {any} */ (Class)[key]
62
+ if (!meta || !Array.isArray(meta.options)) return val
63
+
64
+ let SubClass = null
65
+ let isExplicit = false
66
+ if (typeof val === 'function' && val.prototype && typeof val.prototype.run === 'function') {
67
+ SubClass = val
68
+ } else if (typeof val === 'string') {
69
+ SubClass = meta.options.find((C) => {
70
+ if (typeof C !== 'function') return false
71
+ const className = typeof C.name === 'string' ? C.name : ''
72
+ const alias =
73
+ (typeof C.alias === 'string' ? C.alias : null) ||
74
+ className.replace(/Command|App/g, '').toLowerCase()
75
+ return alias === val
76
+ })
77
+ isExplicit = !!SubClass
78
+ }
79
+
80
+ if (SubClass) {
81
+ const className = typeof Class.name === 'string' ? Class.name : ''
82
+ const myAlias =
83
+ (typeof /** @type {any} */ (Class).alias === 'string' ? /** @type {any} */ (Class).alias : null) ||
84
+ className.replace(/Command|App/g, '').toLowerCase()
85
+ const fullPath = this._.parentPath ? `${this._.parentPath} ${myAlias}` : myAlias
86
+
87
+ const finalData = resolvePositionalArgs(SubClass, data._positionals || [], data)
88
+ return new SubClass(finalData, { ...this._, parentPath: fullPath, _isExplicit: isExplicit })
89
+ }
90
+
91
+ return val
92
+ }
93
+
94
+ /**
95
+ * Generate help text for the model
96
+ * @param {string} [parentPath]
97
+ * @returns {string}
98
+ */
99
+ generateHelp(parentPath = this._.parentPath) {
100
+ const Class = /** @type {typeof ModelAsApp} */ (this.constructor)
101
+
102
+ // Delegate help to sub-command ONLY if it was explicitly requested via arguments.
103
+ // This prevents "store --help" from showing "store list --help" documentation.
104
+ for (const key in this) {
105
+ const val = /** @type {any} */ (this)[key]
106
+ if (val instanceof ModelAsApp && val['help'] && val._._isExplicit) {
107
+ const className = typeof Class.name === 'string' ? Class.name : ''
108
+ const myAlias =
109
+ (typeof /** @type {any} */ (Class).alias === 'string' ? /** @type {any} */ (Class).alias : null) ||
110
+ className.replace(/Command|App/g, '').toLowerCase()
111
+ const fullPath = parentPath ? `${parentPath} ${myAlias}` : myAlias
112
+ return val.generateHelp(fullPath)
113
+ }
114
+ }
115
+
116
+ const className = typeof Class.name === 'string' ? Class.name : ''
117
+ const myAlias =
118
+ (typeof /** @type {any} */ (Class).alias === 'string' ? /** @type {any} */ (Class).alias : null) ||
119
+ className.replace(/Command|App/g, '').toLowerCase()
120
+ const fullPath = parentPath ? `${parentPath} ${myAlias}` : myAlias
121
+
122
+ const t = this._.t
123
+ const lines = []
124
+
125
+ /** @type {any} */
126
+ const UI =
127
+ typeof (/** @type {any} */ (Class).UI) === 'object' && /** @type {any} */ (Class).UI
128
+ ? /** @type {any} */ (Class).UI
129
+ : {}
130
+
131
+ if (UI.title) {
132
+ lines.push(`# ${UI.icon ? UI.icon + ' ' : ''}${t(UI.title)}`.trim())
133
+ if (UI.description) lines.push(`${t(UI.description)}`)
134
+ lines.push('')
135
+ }
136
+
137
+ const posMeta = []
138
+ const posNames = []
139
+ for (const key in Class) {
140
+ const meta = /** @type {any} */ (Class)[key]
141
+ if (meta && typeof meta === 'object' && meta.help && meta.positional) {
142
+ posNames.push(meta.required ? `<${key}>` : `[${key}]`)
143
+ posMeta.push({ key, meta })
144
+ }
33
145
  }
146
+
147
+ const usageTitle = UI.usageTitle ? t(UI.usageTitle) : 'Usage:'
148
+ lines.push(`## ${usageTitle}`)
149
+ lines.push('```bash')
150
+ const posStr = posNames.length > 0 ? ` ${posNames.join(' ')}` : ''
151
+ lines.push(`${fullPath}${posStr} [options]`.trimEnd())
152
+
153
+ let usageExamples = UI.usageExamples
154
+ if (!usageExamples) {
155
+ for (const key in Class) {
156
+ const meta = /** @type {any} */ (Class)[key]
157
+ if (
158
+ meta &&
159
+ typeof meta === 'object' &&
160
+ meta.positional &&
161
+ (Array.isArray(meta.type) || Array.isArray(meta.options))
162
+ ) {
163
+ usageExamples = []
164
+ const subcommands = Array.isArray(meta.type) ? meta.type : meta.options
165
+ for (const SubCmd of subcommands) {
166
+ if (SubCmd && SubCmd.prototype && SubCmd.prototype.generateHelp) {
167
+ const subClassName = typeof SubCmd.name === 'string' ? SubCmd.name : ''
168
+ const cmdName = SubCmd.alias || subClassName.replace(/Command|App/g, '').toLowerCase()
169
+ const desc = SubCmd.UI?.title ? t(SubCmd.UI.title) : ''
170
+ usageExamples.push(`${fullPath} ${cmdName} ${desc ? `— ${desc}` : ''}`.trim())
171
+ }
172
+ }
173
+ if (usageExamples.length === 0) usageExamples = undefined
174
+ break
175
+ }
176
+ }
177
+ }
178
+
179
+ if (Array.isArray(usageExamples)) {
180
+ let maxLeft = 0
181
+ /** @type {any[]} */
182
+ const parsedExamples = []
183
+ for (const ex of usageExamples) {
184
+ const renderedStr = t(ex, { cmd: fullPath })
185
+ const match = renderedStr.match(/^(.*?)\s+(—|-)\s+(.*)$/)
186
+ if (match) {
187
+ const left = match[1].trim()
188
+ const sep = match[2]
189
+ const right = match[3].trim()
190
+ maxLeft = Math.max(maxLeft, left.length)
191
+ parsedExamples.push({ left, sep, right })
192
+ } else {
193
+ parsedExamples.push({ left: renderedStr.trim(), sep: '', right: '' })
194
+ }
195
+ }
196
+ for (const p of parsedExamples) {
197
+ if (p.right) {
198
+ lines.push(`${p.left.padEnd(maxLeft + 3)}${p.sep} ${p.right}`)
199
+ } else {
200
+ lines.push(p.left)
201
+ }
202
+ }
203
+ }
204
+ lines.push('```')
205
+ lines.push('')
206
+
207
+ if (posMeta.length > 0) {
208
+ lines.push(`## Arguments:`)
209
+ lines.push('```bash')
210
+ let maxPosLen = 0
211
+ for (const p of posMeta) maxPosLen = Math.max(maxPosLen, p.key.length)
212
+ for (const p of posMeta) {
213
+ const desc = t(p.meta.help)
214
+ let defValue = p.meta.default
215
+ if (typeof defValue === 'function' && defValue.prototype) {
216
+ const defClassName = typeof defValue.name === 'string' ? defValue.name : ''
217
+ defValue = defValue.alias || defClassName.replace(/Command|App/g, '').toLowerCase()
218
+ }
219
+ const def = defValue !== undefined ? ` [${defValue}]` : ''
220
+ lines.push(` ${p.key.padEnd(maxPosLen + 2)} - ${desc}${def}`)
221
+ }
222
+ lines.push('```')
223
+ lines.push('')
224
+ }
225
+
226
+ const optionsTitle = t(UI.optionsTitle || 'Options:')
227
+ lines.push(`## ${optionsTitle}`)
228
+ lines.push('```bash')
229
+
230
+ let maxOptLen = 0
231
+ let hasAlias = false
232
+ /** @type {Array<{key: string, meta: any, left: string}>} */
233
+ const parsedOptions = []
234
+
235
+ for (const key in Class) {
236
+ const meta = /** @type {any} */ (Class)[key]
237
+ if (!meta || typeof meta !== 'object' || !meta.help || key === 'UI' || meta.positional)
238
+ continue
239
+ if (meta.alias) hasAlias = true
240
+ }
241
+
242
+ const entries = []
243
+ for (const key in Class) {
244
+ entries.push([key, /** @type {any} */ (Class)[key]])
245
+ }
246
+ const sortedEntries = entries.sort(([a], [b]) => a.localeCompare(b))
247
+
248
+ for (const [key, meta] of sortedEntries) {
249
+ if (!meta || typeof meta !== 'object' || !meta.help || key === 'UI' || meta.positional)
250
+ continue
251
+
252
+ let left
253
+ if (hasAlias) {
254
+ left = meta.alias ? ` -${meta.alias}, --${key}` : ` --${key}`
255
+ } else {
256
+ left = ` --${key}`
257
+ }
258
+
259
+ maxOptLen = Math.max(maxOptLen, left.length)
260
+ parsedOptions.push({ key, meta, left })
261
+ }
262
+
263
+ for (const opt of parsedOptions) {
264
+ let right = t(opt.meta.help)
265
+ if (
266
+ opt.key !== 'help' &&
267
+ opt.meta.default !== undefined &&
268
+ opt.meta.default !== null &&
269
+ typeof opt.meta.default !== 'function' &&
270
+ !Array.isArray(opt.meta.default)
271
+ ) {
272
+ if (['boolean', 'string', 'number'].includes(typeof opt.meta.default)) {
273
+ right += ` [${opt.meta.default}]`
274
+ }
275
+ }
276
+ lines.push(`${opt.left.padEnd(maxOptLen + 2)} - ${right}`)
277
+ }
278
+
279
+ lines.push('```')
280
+ lines.push('')
281
+ return lines.join('\n')
34
282
  }
35
283
 
36
- /** @returns {ModelAsAppOptions} */
37
- get _() {
38
- return this.#appOptions
284
+ /**
285
+ * Execute the model programmatically without a UI adapter.
286
+ * @param {any} [data]
287
+ * @param {Partial<ModelAsAppOptions>} [options]
288
+ * @returns {Promise<any>}
289
+ */
290
+ static async execute(data = {}, options = {}) {
291
+ const app = new this(data, options)
292
+ if (typeof app.run !== 'function') return null
293
+
294
+ let finalData = null
295
+ const gen = app.run()
296
+ let res = await gen.next()
297
+ while (!res.done) {
298
+ const intent = res.value
299
+ if (intent && intent.type === 'result') finalData = intent.data
300
+ res = await gen.next()
301
+ }
302
+ if (res.value && res.value.type === 'result') finalData = res.value.data
303
+ return finalData
39
304
  }
305
+
40
306
  /**
307
+ * Default execution generator.
308
+ * Automatically delegates to the first instantiated subcommand field.
309
+ *
41
310
  * @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
42
311
  */
43
312
  async *run() {
313
+ // 1. Automatic Help Handling (Premium OLMUI style)
314
+ if (/** @type {any} */ (this).help) {
315
+ const content = this.generateHelp()
316
+ const UI = /** @type {any} */ (this.constructor).UI || {}
317
+ const title = UI.title || this.constructor.name
318
+
319
+ if (/** @type {any} */ (this).raw) {
320
+ yield show(content, 'info', /** @type {any} */ ({ format: 'markdown', raw: true }))
321
+ } else {
322
+ yield ask('help', { content, title: `${title} Help`, hint: 'content-viewer' })
323
+ }
324
+ return result({})
325
+ }
326
+
327
+ // 2. Automatic Subcommand Delegation
328
+ for (const key in this) {
329
+ const val = /** @type {any} */ (this)[key]
330
+ if (val instanceof ModelAsApp && val !== this) {
331
+ return yield* val.run()
332
+ }
333
+ }
44
334
  return result({})
45
335
  }
46
336
  }
@@ -0,0 +1,117 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { ModelAsApp } from './ModelAsApp.js'
4
+ import { runGenerator, show, result } from '../index.js'
5
+ import DB from '@nan0web/db'
6
+
7
+ // ─── 1. Mock Models for Testing ───
8
+
9
+ class SubCommand extends ModelAsApp {
10
+ static UI = { title: 'Sub Logic' }
11
+ static alias = 'sub'
12
+ static timeout = {
13
+ help: 'Timeout to cancel',
14
+ default: 333,
15
+ }
16
+ constructor(data = {}, options = {}) {
17
+ super(data, options)
18
+ /** @type {number} Timeout to cancel */ this.timeout
19
+ }
20
+ async *run() {
21
+ yield show('Sub running')
22
+ return result({ ok: true, timeout: this.timeout })
23
+ }
24
+ }
25
+
26
+ /**
27
+ * @property {SubCommand} command
28
+ * @property {boolean} help
29
+ */
30
+ class RootApp extends ModelAsApp {
31
+ static alias = 'root'
32
+ static UI = { title: 'Root App' }
33
+ static command = {
34
+ help: 'Target command',
35
+ options: [SubCommand],
36
+ positional: true,
37
+ }
38
+ static debug = {
39
+ help: 'Debug mode',
40
+ type: 'boolean',
41
+ default: false,
42
+ }
43
+ }
44
+
45
+ // ─── 2. User Stories ───
46
+
47
+ describe('ModelAsApp User Stories', () => {
48
+ it('Story: Universal Help - renders sorted and inherited options', async () => {
49
+ const app = new RootApp()
50
+ const help = app.generateHelp()
51
+ assert.deepStrictEqual(help.split('\n'), [
52
+ '# Root App',
53
+ '',
54
+ '## Usage:',
55
+ '```bash',
56
+ 'root [command] [options]',
57
+ 'root sub — Sub Logic',
58
+ '```',
59
+ '',
60
+ '## Arguments:',
61
+ '```bash',
62
+ ' command - Target command',
63
+ '```',
64
+ '',
65
+ '## Options:',
66
+ '```bash',
67
+ ' --debug - Debug mode [false]',
68
+ ' --help - Show help',
69
+ '```',
70
+ '',
71
+ ])
72
+ })
73
+
74
+ it('Story: Subcommand Auto-Routing - instantiates subcommand from string', async () => {
75
+ const app = new RootApp({ command: 'sub' })
76
+ const cmd = /** @type {any} */ (app).command
77
+ assert.ok(cmd instanceof SubCommand, 'Should auto-instantiate SubCommand')
78
+ assert.equal(cmd._.parentPath, 'root', 'Should inject parentPath')
79
+ })
80
+
81
+ it('Story: Subcommand Help - delegates help to subcommand if requested', async () => {
82
+ // Simulating "root sub --help"
83
+ const app = new RootApp({ command: 'sub', help: true })
84
+ const cmd = /** @type {any} */ (app).command
85
+ // In this case, root.command is instantiated and has help: true
86
+ assert.ok(cmd.help, 'Subcommand should have help: true')
87
+
88
+ const helpText = app.generateHelp()
89
+ assert.ok(helpText.includes('# Sub Logic'), 'Should show subcommand help')
90
+ assert.ok(helpText.includes('Usage:'), 'Should have usage')
91
+ assert.ok(helpText.includes('root sub'), 'Should have correct command path')
92
+ })
93
+
94
+ it('Story: Positional Mapping - resolves positionals into model fields', async () => {
95
+ const db = new DB()
96
+ await db.connect()
97
+
98
+ // We use execute to run without real UI
99
+ const res = await RootApp.execute({ command: 'sub', timeout: '3' }, { db })
100
+ assert.deepEqual(res, { ok: true, timeout: 3 }, 'Should execute subcommand and return result')
101
+ assert.equal(res.timeout, 3)
102
+ })
103
+
104
+ it('Story: OLMUI Lifecycle - flows through runGenerator handlers', async () => {
105
+ const app = new RootApp({ command: 'sub' })
106
+ const events = []
107
+
108
+ const data = await runGenerator(app.run(), {
109
+ ask: async () => ({ value: {}, cancelled: false }),
110
+ show: (i) => { events.push(`show:${i.message || i}`) },
111
+ result: (i) => i.data,
112
+ })
113
+
114
+ assert.equal(events[0], 'show:Sub running', 'Should capture show intent from subcommand')
115
+ assert.deepEqual(data, { ok: true, timeout: 333 }, 'Should capture final result')
116
+ })
117
+ })
@@ -1,7 +1,7 @@
1
1
  import { ModelAsApp } from '../ModelAsApp.js'
2
2
  import { resolvePositionalArgs } from '@nan0web/ui-cli'
3
3
  import SnapshotAuditor from './SnapshotAuditor.js'
4
- import GalleryRenderIntent from './GalleryRenderIntent.js'
4
+ import GalleryRenderCommand from './GalleryRenderCommand.js'
5
5
  import { show, result } from '../../core/Intent.js'
6
6
 
7
7
  export class GalleryCommand extends ModelAsApp {
@@ -14,25 +14,26 @@ export class GalleryCommand extends ModelAsApp {
14
14
  static action = {
15
15
  type: 'string',
16
16
  help: 'Command to run',
17
- options: [SnapshotAuditor, GalleryRenderIntent],
18
- default: SnapshotAuditor.alias || SnapshotAuditor.name,
17
+ options: [SnapshotAuditor, GalleryRenderCommand],
18
+ default: SnapshotAuditor,
19
19
  positional: true,
20
20
  }
21
21
 
22
22
  /**
23
23
  * @param {Partial<GalleryCommand> | Record<string, any>} [data={}]
24
- * @param {import('@nan0web/types').ModelOptions} [options={}]
24
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
25
25
  */
26
26
  constructor(data = {}, options = {}) {
27
27
  super(data, options)
28
- /** @type {string} */ this.action
28
+ /** @type {typeof SnapshotAuditor | typeof GalleryRenderCommand} */ this.action
29
29
  /** @type {string[]} */ this._positionals = []
30
30
  }
31
31
 
32
+ /**
33
+ * @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
34
+ */
32
35
  async *run() {
33
- const TargetAction = GalleryCommand.action.options.find(
34
- (opt) => opt.alias === this.action || opt.name === this.action
35
- )
36
+ const TargetAction = this.action
36
37
 
37
38
  if (!TargetAction) {
38
39
  yield show(this._.t(GalleryCommand.UI.unknownAction, { command: this.action }), 'error')
@@ -1,9 +1,9 @@
1
- import { Model } from '@nan0web/types'
2
1
  import SnapshotRunner from './SnapshotRunner.js'
3
2
 
4
3
  import { show, result } from '../../core/Intent.js'
4
+ import { ModelAsApp } from '../index.js'
5
5
 
6
- export class GalleryRenderIntent extends Model {
6
+ export class GalleryRenderCommand extends ModelAsApp {
7
7
  static alias = 'render'
8
8
 
9
9
  static UI = {
@@ -11,36 +11,35 @@ export class GalleryRenderIntent extends Model {
11
11
  success: '✅ Gallery render complete',
12
12
  failed: '🚨 Gallery render failed: {error}',
13
13
  }
14
-
15
- static dataDir = {
16
- type: 'string',
14
+
15
+ static dataDir = {
16
+ type: 'string',
17
17
  default: 'docs/data',
18
18
  help: 'Path to source models directory'
19
19
  }
20
-
21
- static dir = {
22
- type: 'string',
20
+
21
+ static dir = {
22
+ type: 'string',
23
23
  default: 'snapshots/core',
24
24
  help: 'Path to output snapshots directory'
25
25
  }
26
26
 
27
27
  /**
28
-
29
- * @param {Partial<GalleryRenderIntent> | Record<string, any>} [data={}]
30
-
31
- * @param {import('@nan0web/types').ModelOptions} [options={}]
32
-
28
+ * @param {Partial<GalleryRenderCommand> | Record<string, any>} [data={}]
29
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
33
30
  */
34
-
35
31
  constructor(data = {}, options = {}) {
36
32
  super(data, options)
37
33
  /** @type {string} */ this.dataDir
38
34
  /** @type {string} */ this.dir
39
35
  }
40
36
 
37
+ /**
38
+ * @returns {AsyncGenerator<import('../../core/Intent.js').Intent, import('../../core/Intent.js').ResultIntent, any>}
39
+ */
41
40
  async *run() {
42
- yield show(this._.t(GalleryRenderIntent.UI.rendering, { dataDir: this.dataDir, dir: this.dir }))
43
-
41
+ yield show(this._.t(GalleryRenderCommand.UI.rendering, { dataDir: this.dataDir, dir: this.dir }))
42
+
44
43
  const snapshotRunner = new SnapshotRunner({
45
44
  dataDir: this.dataDir,
46
45
  snapshotsDir: this.dir,
@@ -62,16 +61,17 @@ export class GalleryRenderIntent extends Model {
62
61
  try {
63
62
  const res = yield* snapshotRunner.run()
64
63
  if (res.data && res.data.success) {
65
- yield show(this._.t(GalleryRenderIntent.UI.success, {}))
64
+ yield show(this._.t(GalleryRenderCommand.UI.success, {}))
66
65
  } else {
67
- yield show(this._.t(GalleryRenderIntent.UI.failed, { error: 'Audit failed' }), 'error')
66
+ yield show(this._.t(GalleryRenderCommand.UI.failed, { error: 'Audit failed' }), 'error')
68
67
  return result({ status: 'error' })
69
68
  }
70
69
  } catch (error) {
71
- yield show(this._.t(GalleryRenderIntent.UI.failed, { error: /** @type {Error} */ (error).message }), 'error')
70
+ yield show(this._.t(GalleryRenderCommand.UI.failed, { error: /** @type {Error} */ (error).message }), 'error')
72
71
  return result({ status: 'error' })
73
72
  }
73
+ return result({})
74
74
  }
75
75
  }
76
76
 
77
- export default GalleryRenderIntent
77
+ export default GalleryRenderCommand