@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,12 +1,11 @@
1
1
  import { NaN0 } from '@nan0web/types'
2
- import { AuditorModel } from '@nan0web/inspect'
3
- import { progress, result, show } from '../../core/Intent.js'
2
+ import { AuditorModel } from '@nan0web/inspect/domain/AuditorModel'
3
+
4
+ import { result, show } from '../../core/Intent.js'
4
5
 
5
6
  /**
6
7
  * SnapshotAuditor — Zero-Hallucination Snapshot Validation (Model-as-Schema v2).
7
8
  * Parses snapshots without evaluating the app logic and detects artifacts.
8
- *
9
- * @extends {AuditorModel}
10
9
  */
11
10
  export class SnapshotAuditor extends AuditorModel {
12
11
  static alias = 'audit'
@@ -24,7 +23,6 @@ export class SnapshotAuditor extends AuditorModel {
24
23
  default: 'data',
25
24
  }
26
25
 
27
- /** @type {Object<string, string>} Messages for UI */
28
26
  static UI = {
29
27
  title: 'Snapshot Auditor',
30
28
  description: 'Validates UI snapshots against hallucinations and localization leaks.',
@@ -36,6 +34,7 @@ export class SnapshotAuditor extends AuditorModel {
36
34
  auditPassed: 'Audit passed: {file}',
37
35
  auditFailed: 'Audit failed for {file}: {errors}',
38
36
 
37
+ errorDb: 'Database not provided to auditor',
39
38
  errorGlitch: 'Filename "{filename}" has multiple consecutive separators (glitch detected).',
40
39
  errorShort: 'Filename "{filename}" is too short.',
41
40
  errorSyntax: 'Syntax Error: Failed to parse NaN0 file. {msg}',
@@ -56,7 +55,24 @@ export class SnapshotAuditor extends AuditorModel {
56
55
  static ARTIFACTS = ['[object Object]', 'undefined', 'NaN']
57
56
 
58
57
  /** @type {string[]} Words to ignore across all languages */
59
- static EXEMPT_WORDS = ['true', 'false', 'value', 'max', 'min', 'step', 'open', 'first', 'what', 'how', 'start', 'code', 'successfully', 'enter', 'with', 'system']
58
+ static EXEMPT_WORDS = [
59
+ 'true',
60
+ 'false',
61
+ 'value',
62
+ 'max',
63
+ 'min',
64
+ 'step',
65
+ 'open',
66
+ 'first',
67
+ 'what',
68
+ 'how',
69
+ 'start',
70
+ 'code',
71
+ 'successfully',
72
+ 'enter',
73
+ 'with',
74
+ 'system',
75
+ ]
60
76
 
61
77
  /** @type {RegExp} Pattern for suspicious filenames */
62
78
  static SUSPICIOUS_FILENAME = /__|--/
@@ -64,14 +80,24 @@ export class SnapshotAuditor extends AuditorModel {
64
80
  /** @type {number} Minimum filename length */
65
81
  static MIN_FILENAME_LENGTH = 3
66
82
 
83
+ /** @type {string[]} Directories to ignore during scanning */
84
+ static IGNORE_DIRS = ['node_modules', '.git', '.venv', '.datasets', 'dist', 'build', 'types']
85
+
86
+ /**
87
+ * Checks if a directory or file should be ignored.
88
+ * @param {string} name
89
+ * @returns {boolean}
90
+ */
91
+ static isIgnored(name) {
92
+ return name.startsWith('.') || SnapshotAuditor.IGNORE_DIRS.includes(name)
93
+ }
94
+
67
95
  /**
68
96
  * @param {Partial<SnapshotAuditor> | Record<string, any>} [data={}]
69
- * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
97
+ * @param {Partial<import('@nan0web/ui').ModelAsAppOptions>} [options={}]
70
98
  */
71
99
  constructor(data = {}, options = {}) {
72
100
  super(data, options)
73
- /** @type {import('@nan0web/types').ModelOptions} */
74
- this.options = options
75
101
  /** @type {string} Target directory to audit */ this.dir
76
102
  /** @type {string} Directory to scan for dictionaries */ this.data
77
103
  }
@@ -96,7 +122,7 @@ export class SnapshotAuditor extends AuditorModel {
96
122
 
97
123
  /**
98
124
  * Scans data directories to build a word set for each language.
99
- * @param {any} fsDb FileSystem DB.
125
+ * @param {import('@nan0web/db').DB} fsDb FileSystem DB.
100
126
  * @param {string} data
101
127
  * @returns {Promise<Record<string, Set<string>>>}
102
128
  */
@@ -104,106 +130,74 @@ export class SnapshotAuditor extends AuditorModel {
104
130
  /** @type {Record<string, Set<string>>} */
105
131
  const dicts = {}
106
132
 
107
- let entries = []
108
133
  try {
109
- let entriesList;
110
- try {
111
- entriesList = await fsDb.listDir(data)
112
- } catch (e) {
113
- if (/** @type {any} */ (e).code === 'ENOENT' && !data.startsWith('../')) {
114
- entriesList = await fsDb.listDir('../' + data)
115
- } else {
116
- throw e;
117
- }
118
- }
119
- for (const e of entriesList) entries.push(e)
120
- } catch (e) {
121
- return dicts
122
- }
123
-
124
- for (const entry of entries) {
125
- if (entry.stat.isDirectory && entry.name !== '_') {
126
- const lang = entry.name
127
- if (!dicts[lang]) dicts[lang] = new Set()
128
-
129
- const scanLang = async (dirPath) => {
130
- let files = []
131
- try {
132
- const entries = await fsDb.listDir(dirPath)
133
- for (const f of entries) files.push(f)
134
- } catch (e) {
135
- return
134
+ // Find all language directories
135
+ const entries = await fsDb
136
+ .listDir(data)
137
+ .catch(async (e) => {
138
+ if (/** @type {any} */ (e).code === 'ENOENT' && !data.startsWith('../')) {
139
+ return await fsDb.listDir('../' + data)
136
140
  }
137
-
138
- for (const f of files) {
139
- if (f.stat.isDirectory) {
140
- await scanLang(f.path)
141
- } else {
141
+ throw e
142
+ })
143
+ .catch(() => [])
144
+
145
+ for (const entry of entries) {
146
+ if (entry.stat.isDirectory && entry.name !== '_') {
147
+ const lang = entry.name
148
+ if (!dicts[lang]) dicts[lang] = new Set()
149
+
150
+ // Use browse for deep dictionary scanning
151
+ for await (const f of fsDb.browse(entry.path, { depth: Infinity })) {
152
+ if (SnapshotAuditor.isIgnored(f.name)) continue
153
+ if (f.isFile) {
142
154
  try {
143
- const _fsDb = /** @type {any} */ (fsDb)
144
- const raw = _fsDb.FS ? await _fsDb.FS.loadTXT(_fsDb.location(f.path), '', true) : await fsDb.fetch(f.path)
155
+ const raw = await fsDb.fetch(f.path)
145
156
  SnapshotAuditor.extractWords(raw, dicts[lang])
146
157
  } catch (e) {}
147
158
  }
148
159
  }
149
160
  }
150
- await scanLang(entry.path)
151
161
  }
162
+ } catch (e) {
163
+ // Ignore dictionary errors
152
164
  }
153
165
  return dicts
154
166
  }
155
167
 
156
168
  /**
157
169
  * Run the snapshot audit inside the target directory.
158
- * @returns {AsyncGenerator<import('@nan0web/ui').Intent, any, any>}
170
+ * @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
159
171
  */
160
172
  async *run() {
161
- const { t } = this.options
162
- const snapshotsDir = this.dir || '.'
163
-
164
- yield show(t(SnapshotAuditor.UI.starting, { dir: snapshotsDir }))
173
+ const { t } = this._
165
174
 
166
- const files = []
175
+ yield show(t(SnapshotAuditor.UI.starting, { dir: this.dir }))
167
176
 
168
- /** @type {import('@nan0web/db').DB} */
169
- let fsDb = this.options.db
170
- if (fsDb && fsDb.mounts && fsDb.mounts.has('')) {
171
- fsDb = /** @type {import('@nan0web/db').DB} */ (fsDb.mounts.get(''))
172
- }
177
+ const fsDb = this._.db
173
178
 
174
179
  if (!fsDb) {
175
- yield show('FS Database not provided to auditor', 'error')
180
+ yield show(t(SnapshotAuditor.UI.errorDb), 'error')
176
181
  return result({ success: false })
177
182
  }
178
183
 
179
- const findSnapshots = async (dir) => {
180
- try {
181
- let entries;
182
- try {
183
- entries = await fsDb.listDir(dir)
184
- } catch (e) {
185
- if (/** @type {any} */ (e).code === 'ENOENT' && !dir.startsWith('../')) {
186
- entries = await fsDb.listDir('../' + dir)
187
- } else {
188
- throw e;
189
- }
190
- }
191
- for (const entry of entries) {
192
- if (entry.stat.isDirectory) {
193
- await findSnapshots(entry.path)
194
- } else if (entry.name.endsWith('.nan0') || entry.name.endsWith('.txt')) {
195
- files.push(entry.path)
196
- }
184
+ const files = []
185
+ const snapshotsDir = '@app/' + (this.dir || 'snapshots/core')
186
+
187
+ // Use robust DB.browse for recursive snapshot detection
188
+ try {
189
+ for await (const entry of fsDb.browse(snapshotsDir, { depth: Infinity })) {
190
+ if (SnapshotAuditor.isIgnored(entry.name)) continue
191
+ if (entry.isFile && (entry.name.endsWith('.nan0') || entry.name.endsWith('.txt'))) {
192
+ files.push(entry.path)
197
193
  }
198
- } catch (e) {
199
- console.error('Error reading dir:', dir, e)
200
194
  }
195
+ } catch (e) {
196
+ // Directory might be missing
201
197
  }
202
198
 
203
- await findSnapshots(snapshotsDir)
204
-
205
199
  if (files.length === 0) {
206
- yield show(t(SnapshotAuditor.UI.noSnapshots, { dir: snapshotsDir }), 'error')
200
+ yield show(t(SnapshotAuditor.UI.noSnapshots, { dir: this.dir }), 'error')
207
201
  return result({ success: false })
208
202
  }
209
203
 
@@ -216,8 +210,7 @@ export class SnapshotAuditor extends AuditorModel {
216
210
  const locale = segments[segments.indexOf('core') + 1] || 'uk'
217
211
  const componentName = segments.pop() || ''
218
212
 
219
- const _fsDb = /** @type {any} */ (fsDb)
220
- const content = _fsDb.FS ? await _fsDb.FS.loadTXT(_fsDb.location(file), '', true) : await fsDb.fetch(file)
213
+ const content = await fsDb.fetch(file)
221
214
  const textContent = typeof content === 'string' ? content : JSON.stringify(content)
222
215
 
223
216
  return {
@@ -231,10 +224,13 @@ export class SnapshotAuditor extends AuditorModel {
231
224
  let hasErrors = false
232
225
 
233
226
  for (const { file, audit } of results) {
234
- const displayFile = file.startsWith('../') ? file.slice(3) : file
227
+ const displayFile = file.startsWith('@app/') ? file.slice(5) : file
235
228
  if (audit.score < 100) {
236
229
  const errorMessages = audit.errors.join('; ')
237
- yield show(t(SnapshotAuditor.UI.auditFailed, { file: displayFile, errors: errorMessages }), 'error')
230
+ yield show(
231
+ t(SnapshotAuditor.UI.auditFailed, { file: displayFile, errors: errorMessages }),
232
+ 'error',
233
+ )
238
234
  allErrors.push(...audit.errors.map((e) => ({ file: displayFile, error: e })))
239
235
  hasErrors = true
240
236
  } else {
@@ -36,7 +36,7 @@ export class SnapshotRunner extends Model {
36
36
 
37
37
  /**
38
38
  * @param {Partial<SnapshotRunner> | Record<string, any>} [data={}]
39
- * @param {import('@nan0web/types').ModelOptions} [options={}]
39
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
40
40
  */
41
41
  constructor(data = {}, options = {}) {
42
42
  super(data, options)
@@ -5,6 +5,11 @@ import GalleryCommand from './GalleryCommand.js'
5
5
  import ConfigApp from './ConfigApp.js'
6
6
  import { show, result } from '../../core/Intent.js'
7
7
 
8
+ /**
9
+ * @property {string[]} _positionals
10
+ * @property {string} command Type of command to run
11
+ * @property {boolean} help Show help message
12
+ */
8
13
  export class UIApp extends ModelAsApp {
9
14
  static command = {
10
15
  type: 'string',
@@ -40,39 +45,25 @@ export class UIApp extends ModelAsApp {
40
45
 
41
46
  /**
42
47
  * @param {Partial<UIApp> | Record<string, any>} [data={}]
43
- * @param {import('@nan0web/types').ModelOptions} [options={}]
48
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
44
49
  */
45
50
  constructor(data = {}, options = {}) {
46
51
  super(data, options)
47
- /** @type {string[]} */ this._positionals = []
48
- /** @type {string} Type of command to run */ this.command
49
- /** @type {boolean} Show help message */ this.help
50
52
  }
51
53
 
52
54
  async *run() {
53
55
  const t = this._.t
54
- if (this.help || this.command === 'help') {
55
- yield show(t(UIApp.UI.helpText, undefined))
56
- return result({})
57
- }
56
+ const cmd = /** @type {any} */ (this).command
57
+ const help = /** @type {any} */ (this).help
58
58
 
59
- const TargetCommand = UIApp.command.options.find((opt) =>
60
- [opt.alias, opt.name].includes(this.command),
61
- )
59
+ if (help) return yield* super.run()
62
60
 
63
- if (!TargetCommand) {
64
- yield show(t(UIApp.UI.unknownCommand, { command: this.command }), 'error')
61
+ if (!cmd || !(cmd instanceof ModelAsApp)) {
62
+ yield show(t(UIApp.UI.unknownCommand, { command: cmd }), 'error')
65
63
  return result({ status: 'error' })
66
64
  }
67
65
 
68
- // Pass remaining positionals down to the target action
69
- const nextData = resolvePositionalArgs(
70
- /** @type {any} */ (TargetCommand),
71
- this._positionals || [],
72
- this
73
- )
74
- const intent = new TargetCommand(nextData, this._)
75
- return yield* intent.run()
66
+ return yield* cmd.run()
76
67
  }
77
68
  }
78
69
 
package/src/index.js CHANGED
@@ -5,11 +5,12 @@ import StdOut from './StdOut.js'
5
5
  import View from './View/View.js'
6
6
  import RenderOptions from './View/RenderOptions.js'
7
7
  import FrameProps from './Frame/Props.js'
8
- import Model from './Model/index.js'
8
+ import { Model } from '@nan0web/types'
9
+ import Models from './Model/index.js'
9
10
  import Component from './Component/index.js'
10
11
  import App from './App/index.js'
11
12
 
12
- export { Frame, FrameProps, Locale, StdIn, StdOut, View, RenderOptions, Model, Component, App }
13
+ export { Frame, FrameProps, Locale, StdIn, StdOut, View, RenderOptions, Model, Models, Component, App }
13
14
  export { format } from './format.js'
14
15
  export { default as Navigation } from './domain/Navigation.js'
15
16
 
@@ -24,6 +25,7 @@ export { default as UiMessage } from './core/Message/Message.js'
24
25
  export { default as UiStream } from './core/Stream.js'
25
26
  export { default as Error, CancelError } from './core/Error/index.js'
26
27
  export { default as UiAdapter } from './core/UiAdapter.js'
28
+ export { resolvePositionalArgs } from './core/resolvePositionalArgs.js'
27
29
 
28
30
  // OLMUI Generator Engine
29
31
  /** @typedef {import('./core/Intent.js').LogLevel} LogLevel */
package/src/inspect.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './testing/SnapshotRunner.js'
2
2
  export * from './testing/verifySnapshot.js'
3
+ export { SnapshotAuditor } from './domain/app/SnapshotAuditor.js'
@@ -7,11 +7,12 @@ import DBFS from '@nan0web/db-fs'
7
7
  */
8
8
  export class SnapshotRunner {
9
9
  static async generateAndAudit(options) {
10
+ /** @type {import('@nan0web/db').DB} */
10
11
  const db = options.db || new DBFS({ root: options.dataDir })
11
12
  const runner = new Runner(options, { db })
12
13
  if (options.getCategory) runner.getCategory = options.getCategory
13
14
  if (options.createModelStream) runner.createModelStream = options.createModelStream
14
-
15
+
15
16
  const gen = runner.run()
16
17
  let res = await gen.next()
17
18
  while (!res.done) {
@@ -35,6 +35,43 @@ export class SpecRunner extends ModelAsApp {
35
35
  this.registry
36
36
  }
37
37
 
38
+ /**
39
+ * Convenience method to load a .nan0 file and run a specific scenario.
40
+ *
41
+ * 💡 Note on Expectations:
42
+ * You do NOT need to write manual assertions when using this method.
43
+ * The `for await (const _ of runner.run()) {}` loop drives the generator,
44
+ * but ALL assertions are handled automatically inside `SpecAdapter.js`.
45
+ *
46
+ * Whenever the App yields an intent (`ask`, `show`, `result`), `SpecAdapter`
47
+ * intercepts it and compares it strictly against the next step in the `.nan0` file.
48
+ * - If it matches, the test continues (and `$value` is injected back into the App).
49
+ * - If it mismatches, it throws an `assert.fail()` which fails the Node.js test immediately.
50
+ * - If the App finishes early, it throws an `unhandledSteps` error.
51
+ *
52
+ * @param {string} fileDir The directory containing the file (e.g., import.meta.dirname)
53
+ * @param {string} fileName The name of the .nan0 file
54
+ * @param {string} scenarioName The name of the scenario to run
55
+ * @param {Record<string, any>} registry The Model Class registry
56
+ * @param {Partial<import('../index.js').ModelAsAppOptions>} [options={}] Additional runner context options
57
+ * @throws {Error} If the scenario is missing or if expectations fail during execution
58
+ */
59
+ static async executeFile(fileDir, fileName, scenarioName, registry, options = {}) {
60
+ const DB = (await import('@nan0web/db-fs')).DBFS
61
+ const db = new DB({ root: fileDir })
62
+ const doc = await db.loadDocument(fileName)
63
+ const scenarios = Array.isArray(doc) ? doc : [doc]
64
+ const scenario = scenarios.find((s) => s.name === scenarioName) || scenarios[0]
65
+
66
+ if (!scenario) throw new Error(`Scenario ${scenarioName} not found in ${fileName}`)
67
+ if (!scenario.story) throw new Error(`Scenario ${scenarioName} has no story array`)
68
+
69
+ const runner = new this({ stream: scenario.story, registry }, options)
70
+ for await (const _ of runner.run()) {
71
+ // Iterate completely
72
+ }
73
+ }
74
+
38
75
  /**
39
76
  * @throws {Error}
40
77
  * @returns {AsyncGenerator<import('../core/Intent.js').Intent, import('../core/Intent.js').ResultIntent, any>}
@@ -1,4 +1,4 @@
1
- export default Model;
1
+ export default Models;
2
2
  import User from './User/User.js';
3
3
  import { HeaderModel } from '../domain/HeaderModel.js';
4
4
  import { FooterModel } from '../domain/FooterModel.js';
@@ -26,7 +26,7 @@ import { TimelineModel } from '../domain/components/index.js';
26
26
  import { EmptyStateModel } from '../domain/components/index.js';
27
27
  import { BannerModel } from '../domain/components/index.js';
28
28
  import { ProfileDropdownModel } from '../domain/components/index.js';
29
- declare namespace Model {
29
+ declare namespace Models {
30
30
  export { User };
31
31
  export { HeaderModel };
32
32
  export { FooterModel };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Resolves positional CLI arguments into named model fields.
3
+ *
4
+ * Scans a Model class for `static` field descriptors with `positional: true`.
5
+ * The order of positional fields follows the declaration order of static properties
6
+ * (guaranteed by JavaScript spec for non-integer keys).
7
+ *
8
+ * @example
9
+ * class MyModel {
10
+ * static source = { help: 'Source path', default: '.', positional: true }
11
+ * static target = { help: 'Target path', default: 'out', positional: true }
12
+ * static quiet = { help: 'Quiet mode', default: false, type: 'boolean' }
13
+ * }
14
+ *
15
+ * const data = resolvePositionalArgs(MyModel, ['src/', 'dist/'])
16
+ * // → { source: 'src/', target: 'dist/' }
17
+ *
18
+ * @param {typeof Model} ModelClass - The Model class with static field descriptors.
19
+ * @param {string[]} args - Positional arguments from the CLI (e.g., process.argv positionals).
20
+ * @param {Object} [existing={}] - Existing named options (take priority over positionals).
21
+ * @returns {Object} Merged data object with positional args resolved to named fields.
22
+ */
23
+ export function resolvePositionalArgs(ModelClass: typeof Model, args?: string[], existing?: any): any;
24
+ import { Model } from '@nan0web/types';
@@ -0,0 +1 @@
1
+ export {};
@@ -173,9 +173,9 @@ export class Content extends Model {
173
173
  };
174
174
  /**
175
175
  * @param {ContentData | string} [data={}]
176
- * @param {import('@nan0web/types').ModelOptions} [options={}]
176
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
177
177
  */
178
- constructor(data?: ContentData | string, options?: import("@nan0web/types").ModelOptions);
178
+ constructor(data?: ContentData | string, options?: Partial<import("@nan0web/types").ModelOptions>);
179
179
  /** @type {string|undefined} Content */ content: string | undefined;
180
180
  /** @type {Array<Content>|undefined} Children */ children: Array<Content> | undefined;
181
181
  }
@@ -26,9 +26,9 @@ export class Document extends Model {
26
26
  /**
27
27
  *
28
28
  * @param {Partial<Document>} [data]
29
- * @param {import('@nan0web/types').ModelOptions} [options]
29
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options]
30
30
  */
31
- constructor(data?: Partial<Document>, options?: import("@nan0web/types").ModelOptions);
31
+ constructor(data?: Partial<Document>, options?: Partial<import("@nan0web/types").ModelOptions>);
32
32
  /** @type {string} Title */ title: string;
33
33
  /** @type {Array<Content>} Content */ content: Array<Content>;
34
34
  /** @type {Array<Content>} Layout configuration */ $content: Array<Content>;
@@ -32,9 +32,9 @@ export class HeroModel extends Model {
32
32
  };
33
33
  /**
34
34
  * @param {Partial<HeroModel | Record<string, any>>} [data={}]
35
- * @param {import('@nan0web/types').ModelOptions} [options={}]
35
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
36
36
  */
37
- constructor(data?: Partial<HeroModel | Record<string, any>>, options?: import("@nan0web/types").ModelOptions);
37
+ constructor(data?: Partial<HeroModel | Record<string, any>>, options?: Partial<import("@nan0web/types").ModelOptions>);
38
38
  /** @type {string} Top small badge text ior icon */ badge: string;
39
39
  /** @type {string} Hero heading */ title: string;
40
40
  /** @type {string} Hero secondary text */ subtitle: string;
@@ -1,23 +1,67 @@
1
- /** @typedef {import('@nan0web/types').ModelOptions & { adapter: InputAdapter }} ModelAsAppOptions */
1
+ /**
2
+ * @typedef {Object} AppOptions
3
+ * @property {InputAdapter} adapter
4
+ * @property {string} parentPath
5
+ * @property {boolean} _isExplicit
6
+ */
7
+ /** @typedef {import('@nan0web/types').ModelOptions & AppOptions} ModelAsAppOptions */
2
8
  /**
3
9
  * The model with a run generator.
10
+ * @property {boolean} help Show help
4
11
  */
5
12
  export class ModelAsApp extends Model {
13
+ static help: {
14
+ help: string;
15
+ default: boolean;
16
+ };
17
+ /**
18
+ * Execute the model programmatically without a UI adapter.
19
+ * @param {any} [data]
20
+ * @param {Partial<ModelAsAppOptions>} [options]
21
+ * @returns {Promise<any>}
22
+ */
23
+ static execute(data?: any, options?: Partial<ModelAsAppOptions>): Promise<any>;
6
24
  /**
7
25
  * @param {Partial<ModelAsApp> | Record<string, any>} [data={}]
8
26
  * @param {Partial<ModelAsAppOptions>} [options={}]
9
27
  */
10
28
  constructor(data?: Partial<ModelAsApp> | Record<string, any>, options?: Partial<ModelAsAppOptions>);
11
- /** @returns {ModelAsAppOptions} */
12
- get _(): ModelAsAppOptions;
29
+ /** @type {boolean} Show help */ help: boolean;
30
+ _: {
31
+ adapter: InputAdapter;
32
+ parentPath: string;
33
+ _isExplicit: boolean;
34
+ db: import("@nan0web/db").default | null | undefined;
35
+ plugins: Record<string, any>;
36
+ t: import("@nan0web/types/src/utils/TFunction").TFunction;
37
+ };
38
+ /**
39
+ * Instantiates a subcommand if the value matches one of the options.
40
+ * @param {string} key - Field name.
41
+ * @param {any} val - Current value (string, class, or instance).
42
+ * @param {any} [data={}] - Data to pass to the new instance.
43
+ * @returns {any} Instantiated subcommand or original value.
44
+ */
45
+ _instantiateSubCommand(key: string, val: any, data?: any): any;
46
+ /**
47
+ * Generate help text for the model
48
+ * @param {string} [parentPath]
49
+ * @returns {string}
50
+ */
51
+ generateHelp(parentPath?: string): string;
13
52
  /**
53
+ * Default execution generator.
54
+ * Automatically delegates to the first instantiated subcommand field.
55
+ *
14
56
  * @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
15
57
  */
16
58
  run(): AsyncGenerator<import("@nan0web/ui").Intent, import("@nan0web/ui").ResultIntent, any>;
17
- #private;
18
59
  }
19
- export type ModelAsAppOptions = import("@nan0web/types").ModelOptions & {
60
+ export type AppOptions = {
20
61
  adapter: InputAdapter;
62
+ parentPath: string;
63
+ _isExplicit: boolean;
21
64
  };
65
+ export type ModelAsAppOptions = import("@nan0web/types").ModelOptions & AppOptions;
22
66
  import { Model } from '@nan0web/types';
23
67
  import { InputAdapter } from '../core/InputAdapter.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -6,50 +6,19 @@ export class GalleryCommand extends ModelAsApp {
6
6
  static action: {
7
7
  type: string;
8
8
  help: string;
9
- options: (typeof SnapshotAuditor | typeof GalleryRenderIntent)[];
10
- default: string;
9
+ options: (typeof SnapshotAuditor | typeof GalleryRenderCommand)[];
10
+ default: typeof SnapshotAuditor;
11
11
  positional: boolean;
12
12
  };
13
13
  /**
14
14
  * @param {Partial<GalleryCommand> | Record<string, any>} [data={}]
15
- * @param {import('@nan0web/types').ModelOptions} [options={}]
15
+ * @param {Partial<import('@nan0web/types').ModelOptions>} [options={}]
16
16
  */
17
- constructor(data?: Partial<GalleryCommand> | Record<string, any>, options?: import("@nan0web/types").ModelOptions);
18
- /** @type {string} */ action: string;
17
+ constructor(data?: Partial<GalleryCommand> | Record<string, any>, options?: Partial<import("@nan0web/types").ModelOptions>);
18
+ /** @type {typeof SnapshotAuditor | typeof GalleryRenderCommand} */ action: typeof SnapshotAuditor | typeof GalleryRenderCommand;
19
19
  /** @type {string[]} */ _positionals: string[];
20
- run(): AsyncGenerator<import("../../core/Intent.js").ShowIntent | (import("../../core/Intent.js").AskIntent & {
21
- $value?: any;
22
- $success?: boolean;
23
- $files?: Record<string, string>;
24
- $message?: string;
25
- }) | (import("../../core/Intent.js").ProgressIntent & {
26
- $value?: any;
27
- $success?: boolean;
28
- $files?: Record<string, string>;
29
- $message?: string;
30
- }) | (import("../../core/Intent.js").LogIntent & {
31
- $value?: any;
32
- $success?: boolean;
33
- $files?: Record<string, string>;
34
- $message?: string;
35
- }) | (import("../../core/Intent.js").RenderIntent & {
36
- $value?: any;
37
- $success?: boolean;
38
- $files?: Record<string, string>;
39
- $message?: string;
40
- }) | (import("../../core/Intent.js").AgentIntent & {
41
- $value?: any;
42
- $success?: boolean;
43
- $files?: Record<string, string>;
44
- $message?: string;
45
- }) | (import("../../core/Intent.js").ResultIntent & {
46
- $value?: any;
47
- $success?: boolean;
48
- $files?: Record<string, string>;
49
- $message?: string;
50
- }), any, any>;
51
20
  }
52
21
  export default GalleryCommand;
53
22
  import { ModelAsApp } from '../ModelAsApp.js';
54
23
  import SnapshotAuditor from './SnapshotAuditor.js';
55
- import GalleryRenderIntent from './GalleryRenderIntent.js';
24
+ import GalleryRenderCommand from './GalleryRenderCommand.js';