@nan0web/ui 1.12.2 → 3.1.0

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 (160) hide show
  1. package/README.md +18 -355
  2. package/package.json +36 -22
  3. package/src/Component/index.js +1 -5
  4. package/src/Model/Element.js +183 -0
  5. package/src/Model/index.js +2 -2
  6. package/src/Theme/AppTheme.js +19 -0
  7. package/src/Theme/CustomTheme.js +32 -0
  8. package/src/Theme/DarkLightTheme.js +34 -0
  9. package/src/Theme/Theme.js +25 -0
  10. package/src/Theme/atoms/Avatar.js +20 -0
  11. package/src/Theme/atoms/Badge.js +28 -0
  12. package/src/Theme/atoms/Button.js +88 -0
  13. package/src/Theme/atoms/Checkbox.js +26 -0
  14. package/src/Theme/atoms/Input.js +28 -0
  15. package/src/Theme/atoms/Radio.js +26 -0
  16. package/src/Theme/atoms/Select.js +16 -0
  17. package/src/Theme/atoms/TextArea.js +17 -0
  18. package/src/Theme/atoms/Typography.js +26 -0
  19. package/src/Theme/atoms/index.js +11 -0
  20. package/src/Theme/createTheme.js +22 -0
  21. package/src/Theme/index.js +20 -0
  22. package/src/Theme/molecules/Card.js +24 -0
  23. package/src/Theme/molecules/index.js +3 -0
  24. package/src/Theme/organisms/Modal.js +24 -0
  25. package/src/Theme/organisms/index.js +3 -0
  26. package/src/Theme/presets/HighContrastTheme.js +65 -0
  27. package/src/Theme/presets/NightTheme.js +66 -0
  28. package/src/Theme/presets/index.js +4 -0
  29. package/src/Theme/tokens.js +115 -0
  30. package/src/core/InputAdapter.js +1 -2
  31. package/src/core/Intent.js +22 -8
  32. package/src/core/Message/Message.js +3 -0
  33. package/src/core/OutputAdapter.js +9 -13
  34. package/src/core/index.js +7 -4
  35. package/src/core/resolvePositionalArgs.js +51 -0
  36. package/src/domain/Content.js +5 -5
  37. package/src/domain/Document.js +1 -1
  38. package/src/domain/HeroModel.js +1 -1
  39. package/src/domain/ModelAsApp.js +310 -20
  40. package/src/domain/ModelAsApp.story.js +117 -0
  41. package/src/domain/app/GalleryCommand.js +9 -8
  42. package/src/domain/app/{GalleryRenderIntent.js → GalleryRenderCommand.js} +20 -20
  43. package/src/domain/app/IntentAuditor.js +53 -0
  44. package/src/domain/app/JsIntentAuditor.js +145 -0
  45. package/src/domain/app/PyIntentAuditor.js +144 -0
  46. package/src/domain/app/SnapshotAuditor.js +82 -86
  47. package/src/domain/app/SnapshotRunner.js +1 -1
  48. package/src/domain/app/UIApp.js +12 -21
  49. package/src/domain/components/ShellModel.js +2 -2
  50. package/src/index.js +38 -10
  51. package/src/inspect.js +4 -0
  52. package/src/testing/SnapshotRunner.js +2 -1
  53. package/src/utils/format.js +21 -0
  54. package/src/utils/processI18n.js +27 -0
  55. package/src/utils/resolveContext.js +79 -0
  56. package/types/Component/index.d.ts +1 -5
  57. package/types/Model/Element.d.ts +87 -0
  58. package/types/Model/index.d.ts +2 -2
  59. package/types/Theme/AppTheme.d.ts +14 -0
  60. package/types/Theme/CustomTheme.d.ts +21 -0
  61. package/types/Theme/DarkLightTheme.d.ts +16 -0
  62. package/types/Theme/Theme.d.ts +18 -0
  63. package/types/Theme/atoms/Avatar.d.ts +14 -0
  64. package/types/Theme/atoms/Badge.d.ts +22 -0
  65. package/types/Theme/atoms/Button.d.ts +144 -0
  66. package/types/Theme/atoms/Checkbox.d.ts +20 -0
  67. package/types/Theme/atoms/Input.d.ts +22 -0
  68. package/types/Theme/atoms/Radio.d.ts +20 -0
  69. package/types/Theme/atoms/Select.d.ts +15 -0
  70. package/types/Theme/atoms/TextArea.d.ts +17 -0
  71. package/types/Theme/atoms/Typography.d.ts +47 -0
  72. package/types/Theme/atoms/index.d.ts +10 -0
  73. package/types/Theme/createTheme.d.ts +7 -0
  74. package/types/Theme/index.d.ts +10 -0
  75. package/types/Theme/molecules/Card.d.ts +18 -0
  76. package/types/Theme/molecules/index.d.ts +2 -0
  77. package/types/Theme/organisms/Modal.d.ts +18 -0
  78. package/types/Theme/organisms/index.d.ts +2 -0
  79. package/types/Theme/presets/HighContrastTheme.d.ts +2 -0
  80. package/types/Theme/presets/NightTheme.d.ts +2 -0
  81. package/types/Theme/presets/index.d.ts +3 -0
  82. package/types/Theme/tokens.d.ts +119 -0
  83. package/types/core/Intent.d.ts +10 -7
  84. package/types/core/Message/Message.d.ts +3 -0
  85. package/types/core/OutputAdapter.d.ts +2 -4
  86. package/types/core/index.d.ts +5 -2
  87. package/types/core/resolvePositionalArgs.d.ts +24 -0
  88. package/types/docs/README.md.d.ts +1 -0
  89. package/types/domain/Content.d.ts +2 -2
  90. package/types/domain/Document.d.ts +4 -3
  91. package/types/domain/FooterModel.d.ts +2 -1
  92. package/types/domain/HeroModel.d.ts +2 -2
  93. package/types/domain/ModelAsApp.d.ts +49 -5
  94. package/types/domain/ModelAsApp.story.d.ts +1 -0
  95. package/types/domain/app/GalleryCommand.d.ts +6 -37
  96. package/types/domain/app/GalleryRenderCommand.d.ts +27 -0
  97. package/types/domain/app/IntentAuditor.d.ts +23 -0
  98. package/types/domain/app/JsIntentAuditor.d.ts +22 -0
  99. package/types/domain/app/PyIntentAuditor.d.ts +22 -0
  100. package/types/domain/app/SnapshotAuditor.d.ts +34 -25
  101. package/types/domain/app/SnapshotRunner.d.ts +2 -2
  102. package/types/domain/app/UIApp.d.ts +14 -11
  103. package/types/domain/components/ShellModel.d.ts +1 -5
  104. package/types/index.d.ts +10 -10
  105. package/types/inspect.d.ts +4 -0
  106. package/types/testing/verifySnapshot.d.ts +1 -1
  107. package/types/utils/format.d.ts +5 -0
  108. package/types/utils/processI18n.d.ts +8 -0
  109. package/types/utils/resolveContext.d.ts +21 -0
  110. package/src/App/Command/DepsCommand.js +0 -24
  111. package/src/App/Core/CoreApp.js +0 -125
  112. package/src/App/Core/UI.js +0 -63
  113. package/src/App/Core/Widget.js +0 -61
  114. package/src/App/Core/index.js +0 -11
  115. package/src/App/Scenario.js +0 -45
  116. package/src/App/User/Command/Message.js +0 -3
  117. package/src/App/User/Command/index.js +0 -5
  118. package/src/App/User/UserApp.js +0 -85
  119. package/src/App/User/UserUI.js +0 -20
  120. package/src/App/User/index.js +0 -9
  121. package/src/App/index.js +0 -14
  122. package/src/Component/Process/Input.js +0 -63
  123. package/src/Component/Process/Process.js +0 -24
  124. package/src/Component/Process/index.js +0 -5
  125. package/src/Component/Welcome/Input.js +0 -48
  126. package/src/Component/Welcome/Welcome.js +0 -22
  127. package/src/Component/Welcome/index.js +0 -5
  128. package/src/Frame/Frame.js +0 -608
  129. package/src/Frame/Props.js +0 -96
  130. package/src/StdIn.js +0 -100
  131. package/src/StdOut.js +0 -95
  132. package/src/View/RenderOptions.js +0 -48
  133. package/src/View/View.js +0 -306
  134. package/src/core/Message/index.js +0 -6
  135. package/types/App/Command/DepsCommand.d.ts +0 -14
  136. package/types/App/Core/CoreApp.d.ts +0 -70
  137. package/types/App/Core/UI.d.ts +0 -38
  138. package/types/App/Core/Widget.d.ts +0 -39
  139. package/types/App/Core/index.d.ts +0 -10
  140. package/types/App/Scenario.d.ts +0 -26
  141. package/types/App/User/Command/Message.d.ts +0 -2
  142. package/types/App/User/Command/index.d.ts +0 -3
  143. package/types/App/User/UserApp.d.ts +0 -41
  144. package/types/App/User/UserUI.d.ts +0 -9
  145. package/types/App/User/index.d.ts +0 -8
  146. package/types/App/index.d.ts +0 -12
  147. package/types/Component/Process/Input.d.ts +0 -48
  148. package/types/Component/Process/Process.d.ts +0 -13
  149. package/types/Component/Process/index.d.ts +0 -4
  150. package/types/Component/Welcome/Input.d.ts +0 -34
  151. package/types/Component/Welcome/Welcome.d.ts +0 -13
  152. package/types/Component/Welcome/index.d.ts +0 -4
  153. package/types/Frame/Frame.d.ts +0 -186
  154. package/types/Frame/Props.d.ts +0 -77
  155. package/types/StdIn.d.ts +0 -62
  156. package/types/StdOut.d.ts +0 -52
  157. package/types/View/RenderOptions.d.ts +0 -29
  158. package/types/View/View.d.ts +0 -124
  159. package/types/core/Message/index.d.ts +0 -4
  160. package/types/domain/app/GalleryRenderIntent.d.ts +0 -31
@@ -0,0 +1,145 @@
1
+ import { AuditorModel } from '@nan0web/inspect/domain/AuditorModel'
2
+ import { result, show, progress } from '../../core/Intent.js'
3
+ import { IntentAuditor } from './IntentAuditor.js'
4
+
5
+ /**
6
+ * JsIntentAuditor — Specialized auditor for JS/TS output hygiene.
7
+ */
8
+ export class JsIntentAuditor extends AuditorModel {
9
+ /** @type {string[]} Directories to ignore during scanning */
10
+ static IGNORE_DIRS = ['node_modules', '.git', '.venv', '.datasets', 'dist', 'build', 'types', 'play', 'test']
11
+
12
+ /**
13
+ * Checks if a directory or file should be ignored.
14
+ * @param {string} name
15
+ * @returns {boolean}
16
+ */
17
+ static isIgnored(name) {
18
+ return name.startsWith('.') || JsIntentAuditor.IGNORE_DIRS.includes(name)
19
+ }
20
+
21
+ /**
22
+ * Run the JS-specific intent audit.
23
+ * @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
24
+ */
25
+ async *run() {
26
+ const { t } = /** @type {any} */ (this)._
27
+ yield show(t(IntentAuditor.UI.starting, { dir: /** @type {any} */ (this).dir }))
28
+
29
+ const fsDb = /** @type {any} */ (this)._.db
30
+ if (!fsDb) {
31
+ yield show(t(IntentAuditor.UI.errorDb), 'error')
32
+ return result({ success: false })
33
+ }
34
+
35
+ const files = []
36
+ const targetDir = fsDb.resolveSync(/** @type {any} */ (this).dir)
37
+
38
+ try {
39
+ for await (const entry of fsDb.browse(targetDir, { depth: Infinity })) {
40
+ if (JsIntentAuditor.isIgnored(entry.name)) continue
41
+
42
+ if (
43
+ entry.isFile &&
44
+ /\.(js|ts|jsx|tsx)$/.test(entry.name) &&
45
+ !entry.name.endsWith('.test.js') &&
46
+ !entry.name.endsWith('.story.js') &&
47
+ !entry.path.includes('/test/') &&
48
+ !entry.path.includes('/play/')
49
+ ) {
50
+ files.push(entry.path)
51
+ }
52
+ }
53
+ } catch (e) {
54
+ // Directory might be missing
55
+ }
56
+
57
+ if (files.length === 0) {
58
+ yield show(t(IntentAuditor.UI.doneSuccess, {}), 'success')
59
+ return result({ success: true })
60
+ }
61
+
62
+ let hasErrors = false
63
+ const allErrors = []
64
+
65
+ for (const file of files) {
66
+ const displayFile = file.startsWith('@app/') ? file.slice(5) : file
67
+ const content = await fsDb.fetch(file)
68
+ const contentString = typeof content === 'string' ? content : JSON.stringify(content)
69
+
70
+ const fileErrors = JsIntentAuditor.inspectFileContent(contentString, t)
71
+ if (fileErrors.length > 0) {
72
+ const errorMessages = fileErrors.join('; ')
73
+ yield show(
74
+ t(IntentAuditor.UI.auditFailed, { file: displayFile, errors: errorMessages }),
75
+ 'error',
76
+ )
77
+ allErrors.push(...fileErrors.map((e) => ({ file: displayFile, error: e })))
78
+ hasErrors = true
79
+ } else {
80
+ yield progress(t(IntentAuditor.UI.auditPassed, { file: displayFile }))
81
+ }
82
+ }
83
+
84
+ if (hasErrors) {
85
+ yield show(t(IntentAuditor.UI.doneErrors, {}), 'error')
86
+ return result({ success: false, errors: allErrors })
87
+ }
88
+
89
+ yield show(t(IntentAuditor.UI.doneSuccess, {}), 'success')
90
+ return result({ success: true })
91
+ }
92
+
93
+ /**
94
+ * Inspects file content for console.* or process.* writes.
95
+ * @param {string} content Content of the file.
96
+ * @param {import('@nan0web/i18n').TFunction} t Translate function.
97
+ * @returns {string[]} List of error messages.
98
+ */
99
+ static inspectFileContent(content, t) {
100
+ const errors = []
101
+ const lines = content.split('\n')
102
+
103
+ lines.forEach((lineText, index) => {
104
+ const lineNum = index + 1
105
+ const trimmed = lineText.trim()
106
+
107
+ // Skip comments
108
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
109
+ return
110
+ }
111
+
112
+ // 1. Thorough console.* statement scan (including table, trace, etc.)
113
+ const consoleMatch = trimmed.match(/console\.(log|error|warn|info|debug|dir|table|trace|assert)\(/)
114
+ if (consoleMatch) {
115
+ const codeBeforeComment = trimmed.split('//')[0]
116
+ if (codeBeforeComment.includes(consoleMatch[0])) {
117
+ errors.push(
118
+ t(IntentAuditor.UI.errorConsoleLeak, {
119
+ line: lineNum,
120
+ match: consoleMatch[0] + '...',
121
+ }),
122
+ )
123
+ }
124
+ }
125
+
126
+ // 2. process.stdout/stderr write scan
127
+ const processMatch = trimmed.match(/process\.(stdout|stderr)\.write\(/)
128
+ if (processMatch) {
129
+ const codeBeforeComment = trimmed.split('//')[0]
130
+ if (codeBeforeComment.includes(processMatch[0])) {
131
+ errors.push(
132
+ t(IntentAuditor.UI.errorProcessLeak, {
133
+ line: lineNum,
134
+ match: processMatch[0] + '...',
135
+ }),
136
+ )
137
+ }
138
+ }
139
+ })
140
+
141
+ return errors
142
+ }
143
+ }
144
+
145
+ export default JsIntentAuditor
@@ -0,0 +1,144 @@
1
+ import { AuditorModel } from '@nan0web/inspect/domain/AuditorModel'
2
+ import { result, show, progress } from '../../core/Intent.js'
3
+ import { IntentAuditor } from './IntentAuditor.js'
4
+
5
+ /**
6
+ * PyIntentAuditor — Specialized auditor for Python output hygiene.
7
+ */
8
+ export class PyIntentAuditor extends AuditorModel {
9
+ /** @type {string[]} Directories to ignore during scanning */
10
+ static IGNORE_DIRS = ['node_modules', '.git', '.venv', '.datasets', 'dist', 'build', 'types', 'play', 'test', 'tests']
11
+
12
+ /**
13
+ * Checks if a directory or file should be ignored.
14
+ * @param {string} name
15
+ * @returns {boolean}
16
+ */
17
+ static isIgnored(name) {
18
+ return name.startsWith('.') || PyIntentAuditor.IGNORE_DIRS.includes(name)
19
+ }
20
+
21
+ /**
22
+ * Run the Python-specific intent audit.
23
+ * @returns {AsyncGenerator<import('@nan0web/ui').Intent, import('@nan0web/ui').ResultIntent, any>}
24
+ */
25
+ async *run() {
26
+ const { t } = /** @type {any} */ (this)._
27
+ yield show(t(IntentAuditor.UI.starting, { dir: /** @type {any} */ (this).dir }))
28
+
29
+ const fsDb = /** @type {any} */ (this)._.db
30
+ if (!fsDb) {
31
+ yield show(t(IntentAuditor.UI.errorDb), 'error')
32
+ return result({ success: false })
33
+ }
34
+
35
+ const files = []
36
+ const targetDir = fsDb.resolveSync(/** @type {any} */ (this).dir)
37
+
38
+ try {
39
+ for await (const entry of fsDb.browse(targetDir, { depth: Infinity })) {
40
+ if (PyIntentAuditor.isIgnored(entry.name)) continue
41
+
42
+ if (
43
+ entry.isFile &&
44
+ /\.py$/.test(entry.name) &&
45
+ !entry.name.startsWith('test_') &&
46
+ !entry.name.endsWith('_test.py') &&
47
+ !entry.path.includes('/tests/')
48
+ ) {
49
+ files.push(entry.path)
50
+ }
51
+ }
52
+ } catch (e) {
53
+ // Directory might be missing
54
+ }
55
+
56
+ if (files.length === 0) {
57
+ yield show(t(IntentAuditor.UI.doneSuccess, {}), 'success')
58
+ return result({ success: true })
59
+ }
60
+
61
+ let hasErrors = false
62
+ const allErrors = []
63
+
64
+ for (const file of files) {
65
+ const displayFile = file.startsWith('@app/') ? file.slice(5) : file
66
+ const content = await fsDb.fetch(file)
67
+ const contentString = typeof content === 'string' ? content : JSON.stringify(content)
68
+
69
+ const fileErrors = PyIntentAuditor.inspectFileContent(contentString, t)
70
+ if (fileErrors.length > 0) {
71
+ const errorMessages = fileErrors.join('; ')
72
+ yield show(
73
+ t(IntentAuditor.UI.auditFailed, { file: displayFile, errors: errorMessages }),
74
+ 'error',
75
+ )
76
+ allErrors.push(...fileErrors.map((e) => ({ file: displayFile, error: e })))
77
+ hasErrors = true
78
+ } else {
79
+ yield progress(t(IntentAuditor.UI.auditPassed, { file: displayFile }))
80
+ }
81
+ }
82
+
83
+ if (hasErrors) {
84
+ yield show(t(IntentAuditor.UI.doneErrors, {}), 'error')
85
+ return result({ success: false, errors: allErrors })
86
+ }
87
+
88
+ yield show(t(IntentAuditor.UI.doneSuccess, {}), 'success')
89
+ return result({ success: true })
90
+ }
91
+
92
+ /**
93
+ * Inspects Python file content for print or sys.stdout/stderr writes.
94
+ * @param {string} content Content of the file.
95
+ * @param {import('@nan0web/i18n').TFunction} t Translate function.
96
+ * @returns {string[]} List of error messages.
97
+ */
98
+ static inspectFileContent(content, t) {
99
+ const errors = []
100
+ const lines = content.split('\n')
101
+
102
+ lines.forEach((lineText, index) => {
103
+ const lineNum = index + 1
104
+ const trimmed = lineText.trim()
105
+
106
+ // Python Comment skip
107
+ if (trimmed.startsWith('#')) {
108
+ return
109
+ }
110
+
111
+ // 1. Python print statement check
112
+ const printMatch = trimmed.match(/\bprint\(/)
113
+ if (printMatch) {
114
+ const codeBeforeComment = trimmed.split('#')[0]
115
+ if (codeBeforeComment.includes(printMatch[0])) {
116
+ errors.push(
117
+ t(IntentAuditor.UI.errorPrintLeak, {
118
+ line: lineNum,
119
+ match: 'print(...)',
120
+ }),
121
+ )
122
+ }
123
+ }
124
+
125
+ // 2. sys.stdout/stderr write check
126
+ const sysMatch = trimmed.match(/\bsys\.(stdout|stderr)\.write\(/)
127
+ if (sysMatch) {
128
+ const codeBeforeComment = trimmed.split('#')[0]
129
+ if (codeBeforeComment.includes(sysMatch[0])) {
130
+ errors.push(
131
+ t(IntentAuditor.UI.errorSysWriteLeak, {
132
+ line: lineNum,
133
+ match: sysMatch[0] + '...',
134
+ }),
135
+ )
136
+ }
137
+ }
138
+ })
139
+
140
+ return errors
141
+ }
142
+ }
143
+
144
+ export default PyIntentAuditor
@@ -1,15 +1,14 @@
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, progress } 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
- static alias = 'audit'
11
+ static alias = 'snapshots'
13
12
 
14
13
  static dir = {
15
14
  type: 'string',
@@ -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
  }
@@ -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 } = /** @type {any} */ (this)._
165
174
 
166
- const files = []
175
+ yield show(t(SnapshotAuditor.UI.starting, { dir: /** @type {any} */ (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 = /** @type {any} */ (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 = fsDb.resolveSync(/** @type {any} */ (this).dir)
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,14 +224,17 @@ 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 {
241
- yield show(t(SnapshotAuditor.UI.auditPassed, { file: displayFile }), 'success')
237
+ yield progress(t(SnapshotAuditor.UI.auditPassed, { file: displayFile }))
242
238
  }
243
239
  }
244
240
 
@@ -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
 
@@ -37,7 +37,7 @@ export class ShellModel extends Model {
37
37
  ],
38
38
  required: true,
39
39
  }
40
-
40
+
41
41
  static data = {
42
42
  help: 'Data source (DSN)',
43
43
  type: 'string',
@@ -207,7 +207,7 @@ export class ShellModel extends Model {
207
207
  if (!spawn) return yield log('error', 'Spawn missing')
208
208
 
209
209
  const { existsSync } = await import('node:fs')
210
- const viteConfig = existsSync('vite.docs.js') ? 'vite.docs.js' :
210
+ const viteConfig = existsSync('vite.docs.js') ? 'vite.docs.js' :
211
211
  existsSync('vite.config.js') ? 'vite.config.js' : null
212
212
 
213
213
  if (viteConfig) {