@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.1

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 (216) hide show
  1. package/README.md +822 -135
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_boundary_rules.json +50 -0
  5. package/config/notis_app_design_rules.json +135 -0
  6. package/config/portal-route-roots.json +48 -0
  7. package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
  8. package/dist/base-skills/notis-apps/SKILL.md +93 -0
  9. package/dist/base-skills/notis-apps/references/architecture.md +114 -0
  10. package/dist/base-skills/notis-apps/references/context.md +81 -0
  11. package/dist/base-skills/notis-apps/references/design.md +97 -0
  12. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  13. package/dist/base-skills/notis-apps/references/reading.md +67 -0
  14. package/dist/base-skills/notis-apps/references/release.md +107 -0
  15. package/dist/base-skills/notis-apps/references/sdk.md +116 -0
  16. package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
  17. package/dist/base-skills/notis-apps/references/views.md +142 -0
  18. package/dist/base-skills/notis-cli/SKILL.md +154 -0
  19. package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
  20. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  21. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  22. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  23. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  24. package/dist/base-skills/notis-query/SKILL.md +41 -0
  25. package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
  26. package/dist/base-skills/notis-query/references/documents.md +63 -0
  27. package/dist/base-skills/notis-query/references/query.md +76 -0
  28. package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
  29. package/dist/sdk/src/agentContext.ts +36 -0
  30. package/dist/sdk/src/bundleModule.ts +79 -0
  31. package/dist/sdk/src/components/Dialog.tsx +46 -0
  32. package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
  33. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  34. package/dist/sdk/src/components/Markdown.tsx +60 -0
  35. package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
  36. package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  37. package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  38. package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  39. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  40. package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  41. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  42. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  43. package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
  44. package/dist/sdk/src/components/Skeleton.tsx +24 -0
  45. package/dist/sdk/src/config.ts +239 -0
  46. package/dist/sdk/src/documents.ts +258 -0
  47. package/dist/sdk/src/hooks/useAction.ts +28 -0
  48. package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
  49. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  50. package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
  51. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  52. package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
  53. package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  54. package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  55. package/dist/sdk/src/hooks/useDocument.ts +43 -0
  56. package/dist/sdk/src/hooks/useDocuments.ts +84 -0
  57. package/dist/sdk/src/hooks/useHandover.ts +78 -0
  58. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  59. package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
  60. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  61. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  62. package/dist/sdk/src/hooks/useNotis.ts +42 -0
  63. package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
  64. package/dist/sdk/src/hooks/useQuery.ts +74 -0
  65. package/dist/sdk/src/hooks/useTool.ts +65 -0
  66. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  67. package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
  68. package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
  69. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  70. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  71. package/dist/sdk/src/index.ts +200 -0
  72. package/dist/sdk/src/interactions/actions.ts +59 -0
  73. package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
  74. package/dist/sdk/src/interactions/visibility.ts +13 -0
  75. package/dist/sdk/src/interactions.ts +45 -0
  76. package/dist/sdk/src/nativeData.ts +39 -0
  77. package/dist/sdk/src/presentation.tsx +82 -0
  78. package/dist/sdk/src/provider.tsx +49 -0
  79. package/dist/sdk/src/queryCache.ts +172 -0
  80. package/dist/sdk/src/queryErrors.ts +51 -0
  81. package/dist/sdk/src/runtime.ts +603 -0
  82. package/dist/sdk/src/space.ts +425 -0
  83. package/dist/sdk/src/styles.css +290 -0
  84. package/dist/sdk/src/tailwind.ts +66 -0
  85. package/dist/sdk/src/vite.ts +83 -0
  86. package/dist/skill-sync/index.js +2143 -0
  87. package/dist/skill-sync/index.js.map +7 -0
  88. package/dist/skill-sync-worker.mjs +3487 -0
  89. package/dist/space-harness/host.js +66 -0
  90. package/dist/view-renderer/dist/host.css +1 -0
  91. package/dist/view-renderer/dist/host.js +1042 -0
  92. package/dist/view-renderer/dist/theme.css +73 -0
  93. package/dist/view-renderer/src/dom.js +180 -0
  94. package/dist/view-renderer/src/errors.js +8 -0
  95. package/dist/view-renderer/src/host.tsx +173 -0
  96. package/dist/view-renderer/src/index.js +4 -0
  97. package/dist/view-renderer/src/pool.js +76 -0
  98. package/dist/view-renderer/src/remote-broker.js +59 -0
  99. package/dist/view-renderer/src/renderer.js +201 -0
  100. package/package.json +25 -8
  101. package/skills/notis-apps/cli.md +406 -0
  102. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  103. package/skills/notis-onboarding/BRIEF.md +133 -0
  104. package/skills/notis-query/cli.md +39 -0
  105. package/src/agent-hook-entry.js +5 -0
  106. package/src/cli.js +294 -25
  107. package/src/command-specs/agents.js +392 -0
  108. package/src/command-specs/apps.js +1476 -203
  109. package/src/command-specs/auth.js +114 -137
  110. package/src/command-specs/diagnostics.js +730 -0
  111. package/src/command-specs/files.js +29 -0
  112. package/src/command-specs/handover.js +374 -0
  113. package/src/command-specs/helpers.js +86 -82
  114. package/src/command-specs/index.js +31 -6
  115. package/src/command-specs/meta.js +150 -18
  116. package/src/command-specs/onboarding.js +290 -0
  117. package/src/command-specs/profile.js +358 -0
  118. package/src/command-specs/skills.js +304 -0
  119. package/src/command-specs/smoke.js +386 -0
  120. package/src/command-specs/spaces.js +572 -0
  121. package/src/command-specs/tools.js +455 -139
  122. package/src/command-specs/views.js +69 -0
  123. package/src/runtime/agent-browser.js +700 -0
  124. package/src/runtime/agent-memory-state.js +126 -0
  125. package/src/runtime/agent-setup.js +383 -0
  126. package/src/runtime/app-boundary-validator.js +407 -0
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-data-literals.js +82 -0
  129. package/src/runtime/app-platform.js +2752 -237
  130. package/src/runtime/app-registry-scaffolds.js +367 -0
  131. package/src/runtime/app-test-server.js +293 -0
  132. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  133. package/src/runtime/auth-recovery.js +110 -0
  134. package/src/runtime/base-skills.d.ts +20 -0
  135. package/src/runtime/base-skills.js +167 -0
  136. package/src/runtime/channel.js +133 -0
  137. package/src/runtime/confined-space-build.js +74 -0
  138. package/src/runtime/confined-space-verify.js +49 -0
  139. package/src/runtime/delegated-context.js +68 -0
  140. package/src/runtime/errors.js +1 -0
  141. package/src/runtime/git.js +233 -0
  142. package/src/runtime/login-listener.js +15 -0
  143. package/src/runtime/native-record-files.js +47 -0
  144. package/src/runtime/oauth.js +2622 -0
  145. package/src/runtime/output.js +37 -5
  146. package/src/runtime/ports.js +31 -0
  147. package/src/runtime/profiles.js +906 -55
  148. package/src/runtime/retired-sdk-files.js +20 -0
  149. package/src/runtime/screenshot-framing.js +85 -0
  150. package/src/runtime/skill-sync/cloud-client.ts +121 -0
  151. package/src/runtime/skill-sync/index.ts +905 -0
  152. package/src/runtime/skill-sync/local-scanner.ts +1259 -0
  153. package/src/runtime/skill-sync/native-identity.ts +102 -0
  154. package/src/runtime/skill-sync/native-plan.ts +59 -0
  155. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  156. package/src/runtime/skill-sync/sync-plan.ts +54 -0
  157. package/src/runtime/skill-sync/types.ts +157 -0
  158. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  159. package/src/runtime/skill-sync-service.js +109 -0
  160. package/src/runtime/source-root.js +32 -0
  161. package/src/runtime/space-collection-scaffold.js +66 -0
  162. package/src/runtime/space-database-structures.js +229 -0
  163. package/src/runtime/space-fixture-broker.js +50 -0
  164. package/src/runtime/space-lock.js +337 -0
  165. package/src/runtime/space-platform.js +105 -0
  166. package/src/runtime/space-source.js +281 -0
  167. package/src/runtime/space-test-server.js +327 -0
  168. package/src/runtime/space-verification.js +89 -0
  169. package/src/runtime/space-verify-browser-worker.js +51 -0
  170. package/src/runtime/space-view-manifest.js +254 -0
  171. package/src/runtime/store-screenshot.js +146 -0
  172. package/src/runtime/sync-skills.d.ts +37 -0
  173. package/src/runtime/sync-skills.js +231 -0
  174. package/src/runtime/telemetry.js +92 -0
  175. package/src/runtime/transport.js +331 -45
  176. package/src/runtime/view-render.js +49 -0
  177. package/src/skill-sync-worker-entry.js +2 -0
  178. package/src/skill-sync-worker.js +50 -0
  179. package/src/space-harness-host.ts +103 -0
  180. package/template/.harness/index.html.tmpl +435 -0
  181. package/template/CHANGELOG.md +5 -0
  182. package/template/app/globals.css +29 -3
  183. package/template/app/layout.tsx +6 -3
  184. package/template/app/page.tsx +49 -42
  185. package/template/components/page-heading.tsx +23 -0
  186. package/template/components/ui/badge.tsx +7 -4
  187. package/template/components/ui/button.tsx +1 -1
  188. package/template/components/ui/card.tsx +24 -11
  189. package/template/components/ui/native-select.tsx +24 -0
  190. package/template/notis.config.ts +24 -6
  191. package/template/package-lock.json +3642 -0
  192. package/template/package.json +19 -16
  193. package/template/postcss.config.mjs +1 -1
  194. package/template/tailwind.config.ts +1 -6
  195. package/template/tsconfig.json +1 -0
  196. package/src/command-specs/db.js +0 -163
  197. package/src/runtime/app-preview-server.js +0 -312
  198. package/template/packages/notis-sdk/src/config.ts +0 -48
  199. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  200. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  201. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  202. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  203. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  204. package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
  205. package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
  206. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  207. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  208. package/template/packages/notis-sdk/src/index.ts +0 -47
  209. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  210. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  211. package/template/packages/notis-sdk/src/styles.css +0 -123
  212. package/template/packages/notis-sdk/src/vite.ts +0 -54
  213. package/template/packages/notis-sdk/tsconfig.json +0 -15
  214. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
  215. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
  216. /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
@@ -0,0 +1,407 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
2
+ import { dirname, extname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { usageError } from './errors.js';
6
+ import { maskSerializedJsonData } from './app-data-literals.js';
7
+
8
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
9
+
10
+ // Candidate locations for the app boundary rules, in priority order:
11
+ // 1. The monorepo server source (always fresh during local development).
12
+ // 2. The copy bundled into the published package at build time (see
13
+ // scripts/copy-boundary-rules.js + package.json "files": config/). When the
14
+ // CLI is installed via npm, candidate 1 escapes the package and is absent,
15
+ // so the bundled copy is used. This is what was missing before: the package
16
+ // shipped only the escaping path and crashed with ENOENT on any command.
17
+ export const RULES_PATH_CANDIDATES = [
18
+ resolve(moduleDir, '../../../../server/config/notis_app_boundary_rules.json'),
19
+ resolve(moduleDir, '../../config/notis_app_boundary_rules.json'),
20
+ ];
21
+
22
+ export function resolveRulesPath() {
23
+ for (const candidate of RULES_PATH_CANDIDATES) {
24
+ if (existsSync(candidate)) {
25
+ return candidate;
26
+ }
27
+ }
28
+ // Fall back to the bundled-copy path so any error message points inside the
29
+ // package rather than at an escaping monorepo path.
30
+ return RULES_PATH_CANDIDATES[RULES_PATH_CANDIDATES.length - 1];
31
+ }
32
+
33
+ const SOURCE_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.sass', '.pcss']);
34
+ const IGNORED_SOURCE_DIRS = new Set([
35
+ 'node_modules',
36
+ '.git',
37
+ '.next',
38
+ '.notis',
39
+ 'dist',
40
+ 'build',
41
+ 'coverage',
42
+ ]);
43
+
44
+ function compileRules(entries) {
45
+ // Tolerate a rules object whose javascript/css field is a non-array (a corrupt
46
+ // file can parse to {"javascript":"x"}); coerce anything non-array to [] so
47
+ // getCompiledRules degrades to an empty rule set instead of throwing on .map.
48
+ return (Array.isArray(entries) ? entries : []).map((entry) => ({
49
+ regex: new RegExp(entry.pattern, 'm'),
50
+ message: entry.message,
51
+ }));
52
+ }
53
+
54
+ // Loaded lazily on first validation rather than at module import time: a
55
+ // missing rules file must not prevent the whole CLI from booting (every
56
+ // command imports this module transitively). The server re-validates on
57
+ // save/deploy, so an empty client-side rule set degrades gracefully.
58
+ let compiledRules = null;
59
+
60
+ function getCompiledRules() {
61
+ if (compiledRules) {
62
+ return compiledRules;
63
+ }
64
+ let boundaryRules = { javascript: [], css: [] };
65
+ try {
66
+ const parsed = JSON.parse(readFileSync(resolveRulesPath(), 'utf-8'));
67
+ // Keep the safe default unless the file is a real rules object: a valid but
68
+ // non-object payload (null, a number, an array) would otherwise throw on the
69
+ // `.javascript`/`.css` deref below, reintroducing the boot crash this guards.
70
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
71
+ boundaryRules = parsed;
72
+ } else {
73
+ process.stderr.write(
74
+ 'Warning: Notis app boundary rules are not a rules object; skipping local boundary checks.\n',
75
+ );
76
+ }
77
+ } catch (error) {
78
+ process.stderr.write(
79
+ `Warning: could not load Notis app boundary rules (${error.message}); skipping local boundary checks.\n`,
80
+ );
81
+ }
82
+ compiledRules = {
83
+ javascript: compileRules(boundaryRules.javascript),
84
+ css: compileRules(boundaryRules.css),
85
+ };
86
+ return compiledRules;
87
+ }
88
+
89
+ function collectProjectFiles(projectDir, dir, results) {
90
+ if (!existsSync(dir)) {
91
+ return;
92
+ }
93
+
94
+ const entries = readdirSync(dir, { withFileTypes: true });
95
+ for (const entry of entries) {
96
+ if (entry.name.startsWith('.')) {
97
+ if (entry.name !== '.storybook') {
98
+ continue;
99
+ }
100
+ }
101
+
102
+ const fullPath = join(dir, entry.name);
103
+ if (entry.isDirectory()) {
104
+ if (IGNORED_SOURCE_DIRS.has(entry.name)) {
105
+ continue;
106
+ }
107
+ collectProjectFiles(projectDir, fullPath, results);
108
+ continue;
109
+ }
110
+
111
+ if (!SOURCE_FILE_EXTENSIONS.has(extname(entry.name))) {
112
+ continue;
113
+ }
114
+
115
+ results.push({
116
+ path: fullPath,
117
+ relPath: fullPath.slice(projectDir.length + 1),
118
+ content: readFileSync(fullPath, 'utf-8'),
119
+ });
120
+ }
121
+ }
122
+
123
+ function applyRules(content, rules, relPath) {
124
+ const errors = [];
125
+ for (const rule of rules) {
126
+ if (rule.regex.test(content)) {
127
+ errors.push(`${relPath}: ${rule.message}`);
128
+ }
129
+ }
130
+ return errors;
131
+ }
132
+
133
+ function validateTextFile(relPath, content) {
134
+ const extension = extname(relPath);
135
+ const rules = getCompiledRules();
136
+ if (extension === '.css' || extension === '.scss' || extension === '.sass' || extension === '.pcss') {
137
+ return applyRules(content, rules.css, relPath);
138
+ }
139
+ if (extension === '.js' || extension === '.jsx' || extension === '.ts' || extension === '.tsx') {
140
+ return applyRules(maskSerializedJsonData(content, extension), rules.javascript, relPath);
141
+ }
142
+ return [];
143
+ }
144
+
145
+ export function collectProjectBoundaryViolations(projectDir) {
146
+ const files = [];
147
+ collectProjectFiles(projectDir, projectDir, files);
148
+ return files.flatMap((file) => validateTextFile(file.relPath, file.content));
149
+ }
150
+
151
+ export function collectArtifactBoundaryViolations(files) {
152
+ return Object.entries(files).flatMap(([relPath, rawContent]) => {
153
+ const extension = extname(relPath);
154
+ if (!['.js', '.css', '.scss', '.sass', '.pcss'].includes(extension)) {
155
+ return [];
156
+ }
157
+
158
+ const content = Buffer.isBuffer(rawContent)
159
+ ? rawContent.toString('utf-8')
160
+ : typeof rawContent === 'string'
161
+ ? rawContent
162
+ : String(rawContent ?? '');
163
+
164
+ return validateTextFile(relPath, content);
165
+ });
166
+ }
167
+
168
+ export function validateProjectBoundary(projectDir) {
169
+ const violations = collectProjectBoundaryViolations(projectDir);
170
+ if (violations.length > 0) {
171
+ throw usageError(
172
+ `Project violates the Notis app portal boundary:\n${violations.map((error) => ` - ${error}`).join('\n')}`,
173
+ );
174
+ }
175
+ }
176
+
177
+ export function validateArtifactBoundary(files) {
178
+ const violations = collectArtifactBoundaryViolations(files);
179
+ if (violations.length > 0) {
180
+ throw usageError(
181
+ `Built app violates the Notis app portal boundary:\n${violations.map((error) => ` - ${error}`).join('\n')}`,
182
+ );
183
+ }
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Design rules
188
+ //
189
+ // The portal boundary rules above keep an app inside its surface. The design
190
+ // rules below keep it looking like a native, flat Notis page: no bordered
191
+ // boxes around items, no dividers, no palette colors, no eyebrows, no loading
192
+ // text. They are path-scoped, report line numbers, and only ever run on the
193
+ // app's own source files (never on the bundled artifact, where every
194
+ // dependency legitimately contains the word "border").
195
+ //
196
+ // The only override is an inline directive on the same line or the line
197
+ // before the match:
198
+ // // notis-design-allow: <rule-id> <reason of at least N characters>
199
+ // Allowed matches are reported with `allowed: true` so they stay visible.
200
+ // ---------------------------------------------------------------------------
201
+
202
+ export const DESIGN_RULES_PATH_CANDIDATES = [
203
+ resolve(moduleDir, '../../../../server/config/notis_app_design_rules.json'),
204
+ resolve(moduleDir, '../../config/notis_app_design_rules.json'),
205
+ ];
206
+
207
+ export function resolveDesignRulesPath() {
208
+ for (const candidate of DESIGN_RULES_PATH_CANDIDATES) {
209
+ if (existsSync(candidate)) {
210
+ return candidate;
211
+ }
212
+ }
213
+ return DESIGN_RULES_PATH_CANDIDATES[DESIGN_RULES_PATH_CANDIDATES.length - 1];
214
+ }
215
+
216
+ const DEFAULT_DESIGN_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.css'];
217
+
218
+ function globToRegExp(glob) {
219
+ let source = '';
220
+ for (let index = 0; index < glob.length; index += 1) {
221
+ const char = glob[index];
222
+ if (char === '*') {
223
+ if (glob[index + 1] === '*') {
224
+ index += 1;
225
+ if (glob[index + 1] === '/') {
226
+ index += 1;
227
+ source += '(?:.*/)?';
228
+ } else {
229
+ source += '.*';
230
+ }
231
+ } else {
232
+ source += '[^/]*';
233
+ }
234
+ } else if (char === '?') {
235
+ source += '[^/]';
236
+ } else {
237
+ source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
238
+ }
239
+ }
240
+ return new RegExp(`^${source}$`);
241
+ }
242
+
243
+ function matchesAny(relPath, globs) {
244
+ return (globs || []).some((glob) => globToRegExp(glob).test(relPath));
245
+ }
246
+
247
+ let compiledDesignRules = null;
248
+
249
+ export function loadDesignRules({ rulesPath = resolveDesignRulesPath() } = {}) {
250
+ let payload = null;
251
+ try {
252
+ const parsed = JSON.parse(readFileSync(rulesPath, 'utf-8'));
253
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
254
+ payload = parsed;
255
+ }
256
+ } catch (error) {
257
+ process.stderr.write(
258
+ `Warning: could not load Notis app design rules (${error.message}); skipping design checks.\n`,
259
+ );
260
+ }
261
+ if (!payload) {
262
+ return { include: [], exclude: [], extensions: DEFAULT_DESIGN_EXTENSIONS, rules: [], allowDirective: 'notis-design-allow', allowReasonMinLength: 12 };
263
+ }
264
+ return {
265
+ include: Array.isArray(payload.include) ? payload.include : ['app/**', 'components/**'],
266
+ exclude: Array.isArray(payload.exclude) ? payload.exclude : [],
267
+ extensions: Array.isArray(payload.extensions) ? payload.extensions : DEFAULT_DESIGN_EXTENSIONS,
268
+ allowDirective: typeof payload.allow_directive === 'string' ? payload.allow_directive : 'notis-design-allow',
269
+ allowReasonMinLength: Number.isInteger(payload.allow_reason_min_length) ? payload.allow_reason_min_length : 12,
270
+ rules: (Array.isArray(payload.rules) ? payload.rules : []).map((rule) => ({
271
+ id: rule.id,
272
+ severity: rule.severity === 'warn' ? 'warn' : 'error',
273
+ scope: rule.scope === 'file' ? 'file' : 'line',
274
+ regex: new RegExp(rule.pattern, rule.scope === 'file' ? '' : 'gm'),
275
+ message: rule.message,
276
+ include: Array.isArray(rule.include) ? rule.include : null,
277
+ exclude: Array.isArray(rule.exclude) ? rule.exclude : [],
278
+ })),
279
+ };
280
+ }
281
+
282
+ function getCompiledDesignRules() {
283
+ if (!compiledDesignRules) {
284
+ compiledDesignRules = loadDesignRules();
285
+ }
286
+ return compiledDesignRules;
287
+ }
288
+
289
+ function lineNumberAt(content, index) {
290
+ let line = 1;
291
+ for (let cursor = 0; cursor < index; cursor += 1) {
292
+ if (content.charCodeAt(cursor) === 10) line += 1;
293
+ }
294
+ return line;
295
+ }
296
+
297
+ function findAllowDirective(lines, lineNumber, ruleId, directive) {
298
+ const pattern = new RegExp(`${directive}:\\s*${ruleId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(.*)$`);
299
+ for (const candidate of [lines[lineNumber - 1], lines[lineNumber - 2]]) {
300
+ if (typeof candidate !== 'string') continue;
301
+ const match = candidate.match(pattern);
302
+ if (match) {
303
+ return match[1].replace(/\*\/\s*}?\s*$/, '').replace(/-->\s*$/, '').trim();
304
+ }
305
+ }
306
+ return null;
307
+ }
308
+
309
+ export function collectDesignViolationsForFile(relPath, content, config = getCompiledDesignRules()) {
310
+ const normalized = relPath.split('\\').join('/');
311
+ if (!config.extensions.includes(extname(normalized))) return [];
312
+ if (!matchesAny(normalized, config.include)) return [];
313
+ if (matchesAny(normalized, config.exclude)) return [];
314
+
315
+ const checked = ['.js', '.jsx', '.ts', '.tsx'].includes(extname(normalized))
316
+ ? maskSerializedJsonData(content, extname(normalized)) : content;
317
+ const lines = checked.split('\n');
318
+ const violations = [];
319
+ for (const rule of config.rules) {
320
+ if (rule.include && !matchesAny(normalized, rule.include)) continue;
321
+ if (matchesAny(normalized, rule.exclude)) continue;
322
+
323
+ const matches = [];
324
+ if (rule.scope === 'file') {
325
+ const match = rule.regex.exec(checked);
326
+ if (match) matches.push(match.index);
327
+ } else {
328
+ rule.regex.lastIndex = 0;
329
+ let match;
330
+ while ((match = rule.regex.exec(checked)) !== null) {
331
+ matches.push(match.index);
332
+ if (match[0].length === 0) rule.regex.lastIndex += 1;
333
+ }
334
+ }
335
+
336
+ const seenLines = new Set();
337
+ for (const index of matches) {
338
+ const line = lineNumberAt(content, index);
339
+ if (seenLines.has(line)) continue;
340
+ seenLines.add(line);
341
+ const reason = findAllowDirective(lines, line, rule.id, config.allowDirective);
342
+ const allowed = reason !== null && reason.length >= config.allowReasonMinLength;
343
+ violations.push({
344
+ file: normalized,
345
+ line,
346
+ ruleId: rule.id,
347
+ severity: allowed ? 'warn' : rule.severity,
348
+ message: reason !== null && !allowed
349
+ ? `${rule.message} (a notis-design-allow directive needs a reason of at least ${config.allowReasonMinLength} characters)`
350
+ : rule.message,
351
+ allowed,
352
+ reason: allowed ? reason : null,
353
+ });
354
+ }
355
+ }
356
+ return violations;
357
+ }
358
+
359
+ export function collectProjectDesignViolations(projectDir, config = getCompiledDesignRules()) {
360
+ const files = [];
361
+ collectProjectFiles(projectDir, projectDir, files);
362
+ return files.flatMap((file) => collectDesignViolationsForFile(file.relPath, file.content, config));
363
+ }
364
+
365
+ export function collectSourceDesignViolations(files, config = getCompiledDesignRules()) {
366
+ return Object.entries(files).flatMap(([relPath, rawContent]) => {
367
+ const content = Buffer.isBuffer(rawContent)
368
+ ? rawContent.toString('utf-8')
369
+ : typeof rawContent === 'string'
370
+ ? rawContent
371
+ : String(rawContent ?? '');
372
+ return collectDesignViolationsForFile(relPath, content, config);
373
+ });
374
+ }
375
+
376
+ export function formatDesignViolation(violation) {
377
+ const prefix = violation.allowed ? 'allowed' : violation.severity;
378
+ const suffix = violation.allowed ? ` (${violation.reason})` : '';
379
+ return `${violation.file}:${violation.line} [${violation.ruleId}] ${prefix}: ${violation.message}${suffix}`;
380
+ }
381
+
382
+ /**
383
+ * Enforce the design rules on an app's source tree.
384
+ *
385
+ * Returns every violation (allowed ones included) so callers can print
386
+ * warnings. When `enforce` is true (build, verify, screenshot, deploy) any
387
+ * unallowed error-severity violation aborts with a usage error that lists the
388
+ * exact file and line to fix. Dev servers pass `enforce: false` so a
389
+ * half-edited page still reloads.
390
+ */
391
+ export function validateProjectDesign(projectDir, { enforce = true, log = null } = {}) {
392
+ const violations = collectProjectDesignViolations(projectDir);
393
+ const blocking = violations.filter((violation) => !violation.allowed && violation.severity === 'error');
394
+ const nonBlocking = violations.filter((violation) => !blocking.includes(violation));
395
+ if (typeof log === 'function') {
396
+ for (const violation of nonBlocking) log(`[design] ${formatDesignViolation(violation)}`);
397
+ if (!enforce) for (const violation of blocking) log(`[design] ${formatDesignViolation(violation)}`);
398
+ }
399
+ if (enforce && blocking.length > 0) {
400
+ throw usageError(
401
+ `Project violates the Notis app design bar (${blocking.length} issue${blocking.length === 1 ? '' : 's'}). `
402
+ + 'Fix each line below; the only override is an inline "// notis-design-allow: <rule-id> <reason>" comment on the line before.\n'
403
+ + blocking.map((violation) => ` - ${formatDesignViolation(violation)}`).join('\n'),
404
+ );
405
+ }
406
+ return violations;
407
+ }
@@ -0,0 +1,79 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export const APP_CHANGELOG_FILE = 'CHANGELOG.md';
5
+ export const CHANGELOG_MERGE_DATE = '{PR_MERGE_DATE}';
6
+
7
+ const ENTRY_HEADING = /^##\s+\[([^\]]+)]\s+-\s+(\{PR_MERGE_DATE\}|\d{4}-\d{2}-\d{2})\s*$/;
8
+
9
+ function validCalendarDate(value) {
10
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
11
+ const date = new Date(`${value}T00:00:00Z`);
12
+ return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
13
+ }
14
+
15
+ export function parseAppChangelog(markdown, { sourcePath = APP_CHANGELOG_FILE } = {}) {
16
+ const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
17
+ const entries = [];
18
+ const errors = [];
19
+ let current = null;
20
+
21
+ function finishCurrent() {
22
+ if (!current) return;
23
+ entries.push({
24
+ title: current.title,
25
+ date: current.date,
26
+ body: current.body.join('\n').trim(),
27
+ });
28
+ }
29
+
30
+ for (const [index, line] of lines.entries()) {
31
+ // Only a level-2 heading (`## ...`, not `###`/`####`) can start a release
32
+ // entry; deeper sub-section headings are legitimate release-body content.
33
+ if (!/^##(?!#)/.test(line)) {
34
+ if (current) current.body.push(line);
35
+ continue;
36
+ }
37
+
38
+ const match = line.match(ENTRY_HEADING);
39
+ if (!match) {
40
+ errors.push(
41
+ `${sourcePath}:${index + 1} must use \`## [Release title] - YYYY-MM-DD\` or \`## [Release title] - {PR_MERGE_DATE}\`.`,
42
+ );
43
+ continue;
44
+ }
45
+
46
+ finishCurrent();
47
+ const title = match[1].trim();
48
+ const date = match[2];
49
+ if (!title) {
50
+ errors.push(`${sourcePath}:${index + 1} must include a release title inside square brackets.`);
51
+ }
52
+ if (date !== CHANGELOG_MERGE_DATE && !validCalendarDate(date)) {
53
+ errors.push(`${sourcePath}:${index + 1} contains an invalid calendar date.`);
54
+ }
55
+ current = { title, date, body: [] };
56
+ }
57
+ finishCurrent();
58
+
59
+ if (entries.length === 0 && errors.length === 0) {
60
+ errors.push(
61
+ `${sourcePath} must include at least one \`## [Release title] - YYYY-MM-DD\` entry.`,
62
+ );
63
+ }
64
+
65
+ return { source_path: sourcePath, entries, errors, exists: true };
66
+ }
67
+
68
+ export function readAppChangelog(projectDir) {
69
+ const path = join(projectDir, APP_CHANGELOG_FILE);
70
+ if (!existsSync(path)) {
71
+ return {
72
+ source_path: APP_CHANGELOG_FILE,
73
+ entries: [],
74
+ errors: [`${APP_CHANGELOG_FILE} is required for Store publication.`],
75
+ exists: false,
76
+ };
77
+ }
78
+ return parseAppChangelog(readFileSync(path, 'utf-8'));
79
+ }
@@ -0,0 +1,82 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ let babelParse;
5
+
6
+ function isJsonParse(node) {
7
+ const callee = node.callee;
8
+ return node.type === 'CallExpression' && callee?.type === 'MemberExpression' && !callee.computed
9
+ && callee.object?.type === 'Identifier' && callee.object.name === 'JSON'
10
+ && callee.property?.type === 'Identifier' && callee.property.name === 'parse';
11
+ }
12
+
13
+ function dataContext(ancestors) {
14
+ if (ancestors.some(node => node.type === 'JSXAttribute')) return false;
15
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
16
+ const node = ancestors[index];
17
+ if (node.type === 'JSXAttribute' || node.type === 'ImportDeclaration'
18
+ || node.type === 'ImportExpression' || node.type === 'TSImportType'
19
+ || node.type === 'ExportAllDeclaration' || node.type === 'ExportNamedDeclaration'
20
+ || node.type === 'NewExpression' || node.type === 'TaggedTemplateExpression'
21
+ || node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') return false;
22
+ if (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') {
23
+ if (!isJsonParse(node)) return false;
24
+ }
25
+ if (['VariableDeclarator', 'ExportDefaultDeclaration', 'ReturnStatement'].includes(node.type)) return true;
26
+ if (node.type === 'ExpressionStatement') return false;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * Serialized JSON is data, not source: a review may quote forbidden host code
33
+ * or historical styles. Mask only complete object/array JSON literal values in
34
+ * data-expression positions. Keep ordinary strings, import/call/selector and
35
+ * JSX attribute contexts, and every template expression under the existing
36
+ * rules. Parsing errors (or a missing parser) retain the original checks.
37
+ * Source/archive bytes are never rewritten; offsets and line breaks survive.
38
+ */
39
+ export function maskSerializedJsonData(content, extension = '.js') {
40
+ let ast;
41
+ try {
42
+ babelParse ||= require('@babel/parser').parse;
43
+ ast = babelParse(content, {
44
+ sourceType: 'unambiguous',
45
+ allowReturnOutsideFunction: true,
46
+ plugins: [...(['.ts', '.tsx'].includes(extension) ? ['typescript'] : []),
47
+ ...(extension !== '.ts' ? ['jsx'] : [])],
48
+ });
49
+ } catch { return content; }
50
+ const ranges = [];
51
+ const pending = [[ast, []]];
52
+ while (pending.length) {
53
+ const [node, ancestors] = pending.pop();
54
+ if (!node || typeof node !== 'object') continue;
55
+ const literal = node.type === 'StringLiteral' ? node.value
56
+ : node.type === 'TemplateLiteral' && node.expressions.length === 0 ? node.quasis[0]?.value.cooked : null;
57
+ if (typeof literal === 'string' && dataContext(ancestors)) {
58
+ try {
59
+ const value = JSON.parse(literal);
60
+ if (value !== null && typeof value === 'object') {
61
+ ranges.push([node.start, node.end]);
62
+ continue;
63
+ }
64
+ } catch { /* Ordinary/non-JSON strings keep their original checks. */ }
65
+ }
66
+ const parents = [...ancestors, node];
67
+ for (const [key, value] of Object.entries(node)) {
68
+ if (['loc', 'extra', 'tokens', 'comments', 'errors'].includes(key)) continue;
69
+ if (Array.isArray(value)) {
70
+ for (const child of value) if (child?.type) pending.push([child, parents]);
71
+ } else if (value?.type) pending.push([value, parents]);
72
+ }
73
+ }
74
+ let cursor = 0;
75
+ const result = [];
76
+ for (const [start, end] of ranges.sort((a, b) => a[0] - b[0])) {
77
+ result.push(content.slice(cursor, start), content.slice(start, end).replace(/[^\r\n]/g, ' '));
78
+ cursor = end;
79
+ }
80
+ result.push(content.slice(cursor));
81
+ return result.join('');
82
+ }