@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.
- package/README.md +18 -345
- package/package.json +13 -8
- package/src/Model/index.js +2 -2
- package/src/core/GeneratorRunner.js +8 -0
- package/src/core/resolvePositionalArgs.js +51 -0
- package/src/domain/Content.js +5 -5
- package/src/domain/Document.js +1 -1
- package/src/domain/HeroModel.js +1 -1
- package/src/domain/ModelAsApp.js +310 -20
- package/src/domain/ModelAsApp.story.js +117 -0
- package/src/domain/app/GalleryCommand.js +9 -8
- package/src/domain/app/{GalleryRenderIntent.js → GalleryRenderCommand.js} +20 -20
- package/src/domain/app/SnapshotAuditor.js +81 -85
- package/src/domain/app/SnapshotRunner.js +1 -1
- package/src/domain/app/UIApp.js +12 -21
- package/src/index.js +4 -2
- package/src/inspect.js +1 -0
- package/src/testing/SnapshotRunner.js +2 -1
- package/src/testing/SpecRunner.js +37 -0
- package/types/Model/index.d.ts +2 -2
- package/types/core/resolvePositionalArgs.d.ts +24 -0
- package/types/docs/README.md.d.ts +1 -0
- package/types/domain/Content.d.ts +2 -2
- package/types/domain/Document.d.ts +2 -2
- package/types/domain/HeroModel.d.ts +2 -2
- package/types/domain/ModelAsApp.d.ts +49 -5
- package/types/domain/ModelAsApp.story.d.ts +1 -0
- package/types/domain/app/GalleryCommand.d.ts +6 -37
- package/types/domain/app/GalleryRenderCommand.d.ts +27 -0
- package/types/domain/app/SnapshotAuditor.d.ts +33 -23
- package/types/domain/app/SnapshotRunner.d.ts +2 -2
- package/types/domain/app/UIApp.d.ts +14 -11
- package/types/index.d.ts +4 -2
- package/types/inspect.d.ts +1 -0
- package/types/testing/SpecRunner.d.ts +22 -0
- package/types/testing/verifySnapshot.d.ts +1 -1
- 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
|
-
|
|
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 = [
|
|
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/
|
|
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 {
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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,
|
|
170
|
+
* @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
|
|
159
171
|
*/
|
|
160
172
|
async *run() {
|
|
161
|
-
const { t } = this.
|
|
162
|
-
const snapshotsDir = this.dir || '.'
|
|
163
|
-
|
|
164
|
-
yield show(t(SnapshotAuditor.UI.starting, { dir: snapshotsDir }))
|
|
173
|
+
const { t } = this._
|
|
165
174
|
|
|
166
|
-
|
|
175
|
+
yield show(t(SnapshotAuditor.UI.starting, { dir: this.dir }))
|
|
167
176
|
|
|
168
|
-
|
|
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(
|
|
180
|
+
yield show(t(SnapshotAuditor.UI.errorDb), 'error')
|
|
176
181
|
return result({ success: false })
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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:
|
|
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
|
|
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('
|
|
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(
|
|
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)
|
package/src/domain/app/UIApp.js
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
return result({})
|
|
57
|
-
}
|
|
56
|
+
const cmd = /** @type {any} */ (this).command
|
|
57
|
+
const help = /** @type {any} */ (this).help
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
[opt.alias, opt.name].includes(this.command),
|
|
61
|
-
)
|
|
59
|
+
if (help) return yield* super.run()
|
|
62
60
|
|
|
63
|
-
if (!
|
|
64
|
-
yield show(t(UIApp.UI.unknownCommand, { command:
|
|
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
|
-
|
|
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 '
|
|
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
|
@@ -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>}
|
package/types/Model/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export default
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/** @
|
|
12
|
-
|
|
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
|
|
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
|
|
10
|
-
default:
|
|
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 {
|
|
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
|
|
24
|
+
import GalleryRenderCommand from './GalleryRenderCommand.js';
|