@notur/sdk 1.4.6 → 1.4.7

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 (46) hide show
  1. package/README.md +44 -0
  2. package/bin/notur-create.js +19 -13
  3. package/bin/notur-doctor.js +123 -0
  4. package/bin/notur-pack.js +13 -7
  5. package/bin/notur-sync.js +199 -0
  6. package/bin/notur-validate.js +200 -0
  7. package/bin/notur.js +7 -1
  8. package/dist/events.d.ts +10 -0
  9. package/dist/events.d.ts.map +1 -1
  10. package/dist/events.js +7 -0
  11. package/dist/events.js.map +1 -1
  12. package/dist/hooks/useExtensionConfig.d.ts +23 -4
  13. package/dist/hooks/useExtensionConfig.d.ts.map +1 -1
  14. package/dist/hooks/useExtensionConfig.js +8 -1
  15. package/dist/hooks/useExtensionConfig.js.map +1 -1
  16. package/dist/hooks/useNavigate.d.ts +27 -6
  17. package/dist/hooks/useNavigate.d.ts.map +1 -1
  18. package/dist/hooks/useNavigate.js +11 -2
  19. package/dist/hooks/useNavigate.js.map +1 -1
  20. package/dist/hooks/useNoturEvent.d.ts +12 -0
  21. package/dist/hooks/useNoturEvent.d.ts.map +1 -1
  22. package/dist/hooks/useNoturEvent.js +12 -0
  23. package/dist/hooks/useNoturEvent.js.map +1 -1
  24. package/dist/hooks/usePermission.d.ts +10 -1
  25. package/dist/hooks/usePermission.d.ts.map +1 -1
  26. package/dist/hooks/usePermission.js +10 -1
  27. package/dist/hooks/usePermission.js.map +1 -1
  28. package/dist/hooks/useServerContext.d.ts +18 -3
  29. package/dist/hooks/useServerContext.d.ts.map +1 -1
  30. package/dist/hooks/useServerContext.js +8 -1
  31. package/dist/hooks/useServerContext.js.map +1 -1
  32. package/dist/hooks/useUserContext.d.ts +10 -2
  33. package/dist/hooks/useUserContext.d.ts.map +1 -1
  34. package/dist/hooks/useUserContext.js +2 -0
  35. package/dist/hooks/useUserContext.js.map +1 -1
  36. package/dist/index.d.ts +6 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/types.d.ts +179 -20
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js +6 -1
  42. package/dist/types.js.map +1 -1
  43. package/examples/red-button/extension.yaml +0 -10
  44. package/examples/red-button/package.json +5 -2
  45. package/package.json +5 -2
  46. package/examples/red-button/src/RedButtonExtension.php +0 -11
package/README.md CHANGED
@@ -145,6 +145,50 @@ Options:
145
145
  - `--with-api-routes` — include a client API route stub.
146
146
  - `--force` — allow writing into an existing empty directory.
147
147
 
148
+ Frontend-only extensions are manifest-only by default. They do not need a PHP entrypoint unless you choose backend features such as API routes.
149
+
150
+ ### `notur-sync` — Sync generated metadata
151
+
152
+ Reads `extension.yaml` and updates derived local development files.
153
+
154
+ ```bash
155
+ npx notur-sync
156
+ npx @notur/sdk sync
157
+ ```
158
+
159
+ It keeps `package.json` name/version/scripts aligned, creates `.env.example` if missing, and creates `webpack.config.js` for frontend extensions that do not already have one.
160
+
161
+ Options:
162
+ - `--force-webpack` — regenerate `webpack.config.js` from `extension.yaml`.
163
+
164
+ ### `notur-validate` — Validate extension structure
165
+
166
+ Runs local checks before packaging or pushing:
167
+
168
+ ```bash
169
+ npx notur-validate
170
+ npx @notur/sdk validate
171
+ ```
172
+
173
+ Checks include manifest shape, id format, route file paths, bundle path, package metadata drift, missing scripts, `createExtension()` usage, and unknown slot IDs.
174
+
175
+ Options:
176
+ - `--strict` — treat warnings as errors.
177
+
178
+ ### `notur-doctor` — Diagnose local and remote setup
179
+
180
+ Summarizes whether the local extension is buildable and ready to push:
181
+
182
+ ```bash
183
+ npx notur-doctor
184
+ npx @notur/sdk doctor
185
+ ```
186
+
187
+ It checks manifest/package/build files, runs `notur-validate`, and verifies local `.env` or shell variables for remote push.
188
+
189
+ Options:
190
+ - `--no-remote` — skip remote host/key checks.
191
+
148
192
  ### `notur-pack` — Package extensions
149
193
 
150
194
  Creates a `.notur` archive (tar.gz) from your extension directory, ready to upload to the Pterodactyl admin panel.
@@ -281,11 +281,19 @@ function ensureTarget(target, force) {
281
281
  }
282
282
  }
283
283
 
284
- function manifestTemplate({ id, vendor, name, className, namespace, frontend, apiRoutes, display, description }) {
284
+ function manifestTemplate({ id, className, namespace, frontend, apiRoutes, display, description, phpEntrypoint }) {
285
285
  const frontendSection = frontend
286
286
  ? `
287
287
  frontend:
288
288
  bundle: "resources/frontend/dist/extension.js"
289
+ `
290
+ : '';
291
+ const phpSection = phpEntrypoint
292
+ ? `
293
+ entrypoint: "${namespace.replace(/\\/g, '\\\\')}\\\\${className}"
294
+ autoload:
295
+ psr-4:
296
+ "${namespace.replace(/\\/g, '\\\\')}\\\\": "src/"
289
297
  `
290
298
  : '';
291
299
  const backendSection = apiRoutes
@@ -302,16 +310,7 @@ name: "${yamlString(display)}"
302
310
  version: "1.0.0"
303
311
  description: "${yamlString(description)}"
304
312
  license: "MIT"
305
-
306
- requires:
307
- notur: "^1.0"
308
- pterodactyl: "^1.12"
309
- php: "^8.2"
310
-
311
- entrypoint: "${namespace.replace(/\\/g, '\\\\')}\\\\${className}"
312
- autoload:
313
- psr-4:
314
- "${namespace.replace(/\\/g, '\\\\')}\\\\": "src/"
313
+ ${phpSection}
315
314
  ${backendSection}
316
315
  ${frontendSection}`;
317
316
  }
@@ -390,13 +389,16 @@ function packageTemplate(id) {
390
389
  dev: 'webpack-cli --mode development --watch --config webpack.config.js',
391
390
  pack: 'notur-pack',
392
391
  push: 'notur-push',
392
+ sync: 'notur-sync',
393
+ validate: 'notur-validate',
394
+ doctor: 'notur-doctor',
393
395
  },
394
396
  peerDependencies: {
395
397
  react: '^16.14.0',
396
398
  'react-dom': '^16.14.0',
397
399
  },
398
400
  devDependencies: {
399
- '@notur/sdk': '^1.4.5',
401
+ '@notur/sdk': '^1.4.7',
400
402
  '@types/react': '^16.14.0',
401
403
  '@types/react-dom': '^16.9.0',
402
404
  react: '^16.14.0',
@@ -507,6 +509,7 @@ async function main() {
507
509
  const namespace = `${studly(vendor)}\\${studly(name)}`;
508
510
  const className = `${studly(name)}Extension`;
509
511
  const target = path.resolve(options.path, name);
512
+ const needsPhpEntrypoint = options.withApiRoutes;
510
513
 
511
514
  ensureTarget(target, options.force);
512
515
 
@@ -522,8 +525,11 @@ async function main() {
522
525
  apiRoutes: options.withApiRoutes,
523
526
  display: options.displayName || displayName(name),
524
527
  description: options.description || 'A Notur extension',
528
+ phpEntrypoint: needsPhpEntrypoint,
525
529
  }));
526
- writeFile(path.join(target, 'src', `${className}.php`), phpTemplate({ namespace, className }));
530
+ if (needsPhpEntrypoint) {
531
+ writeFile(path.join(target, 'src', `${className}.php`), phpTemplate({ namespace, className }));
532
+ }
527
533
  if (options.withApiRoutes) {
528
534
  writeFile(path.join(target, 'src/routes/api-client.php'), apiRouteTemplate());
529
535
  }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ function parseArgs() {
8
+ const args = process.argv.slice(2);
9
+ const options = { path: '.', remote: true };
10
+ for (let i = 0; i < args.length; i++) {
11
+ const arg = args[i];
12
+ if (arg === '--no-remote') {
13
+ options.remote = false;
14
+ } else if (arg === '--help' || arg === '-h') {
15
+ usage(0);
16
+ } else if (!arg.startsWith('-')) {
17
+ options.path = arg;
18
+ } else {
19
+ console.error(`Unknown argument: ${arg}`);
20
+ usage(1);
21
+ }
22
+ }
23
+ return options;
24
+ }
25
+
26
+ function usage(code) {
27
+ console.log(`Usage:
28
+ npx notur-doctor [path]
29
+ npx @notur/sdk doctor [path]
30
+
31
+ Options:
32
+ --no-remote Skip remote host/key checks`);
33
+ process.exit(code);
34
+ }
35
+
36
+ function parseEnv(basePath) {
37
+ const envPath = path.join(basePath, '.env');
38
+ if (!fs.existsSync(envPath)) return {};
39
+ const values = {};
40
+ for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
41
+ const trimmed = line.trim();
42
+ if (!trimmed || trimmed.startsWith('#')) continue;
43
+ const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
44
+ if (!match) continue;
45
+ values[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, '');
46
+ }
47
+ return values;
48
+ }
49
+
50
+ function check(label, ok, detail = '') {
51
+ const mark = ok ? 'ok' : 'fail';
52
+ console.log(`${mark}: ${label}${detail ? ` - ${detail}` : ''}`);
53
+ return ok;
54
+ }
55
+
56
+ function warn(label, ok, detail = '') {
57
+ const mark = ok ? 'ok' : 'warn';
58
+ console.log(`${mark}: ${label}${detail ? ` - ${detail}` : ''}`);
59
+ return ok;
60
+ }
61
+
62
+ function commandExists(command) {
63
+ const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
64
+ return !result.error && result.status === 0;
65
+ }
66
+
67
+ function main() {
68
+ const options = parseArgs();
69
+ const basePath = path.resolve(options.path);
70
+ let ok = true;
71
+
72
+ console.log(`Notur doctor for ${basePath}`);
73
+
74
+ ok = check('extension directory exists', fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) && ok;
75
+ ok = check('extension.yaml exists', fs.existsSync(path.join(basePath, 'extension.yaml')) || fs.existsSync(path.join(basePath, 'extension.yml'))) && ok;
76
+
77
+ const packagePath = path.join(basePath, 'package.json');
78
+ const hasPackage = fs.existsSync(packagePath);
79
+ check('package.json exists', hasPackage);
80
+ if (hasPackage) {
81
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
82
+ ok = check('build script', Boolean(pkg.scripts?.build)) && ok;
83
+ check('push script', Boolean(pkg.scripts?.push));
84
+ check('@notur/sdk dependency', Boolean(pkg.dependencies?.['@notur/sdk'] || pkg.devDependencies?.['@notur/sdk']));
85
+ }
86
+
87
+ check('node available', Boolean(process.version), process.version);
88
+ check('npm available', commandExists('npm'));
89
+ check('webpack config exists', fs.existsSync(path.join(basePath, 'webpack.config.js')));
90
+
91
+ const bundleCandidates = [
92
+ 'resources/frontend/dist/extension.js',
93
+ 'resources/frontend/dist/bundle.js',
94
+ 'dist/extension.js',
95
+ 'dist/bundle.js',
96
+ ];
97
+ warn('frontend bundle exists', bundleCandidates.some(file => fs.existsSync(path.join(basePath, file))), 'run npm run build before packaging');
98
+
99
+ const validateResult = spawnSync(process.execPath, [path.join(__dirname, 'notur-validate.js'), basePath], {
100
+ stdio: 'pipe',
101
+ encoding: 'utf8',
102
+ });
103
+ ok = check('notur validate', validateResult.status === 0) && ok;
104
+ if (validateResult.stdout.trim()) console.log(validateResult.stdout.trim());
105
+ if (validateResult.stderr.trim()) console.log(validateResult.stderr.trim());
106
+
107
+ if (options.remote) {
108
+ const env = parseEnv(basePath);
109
+ const host = process.env.NOTUR_HOST || env.NOTUR_HOST;
110
+ const key = process.env.NOTUR_PUSH_KEY || process.env.NOTUR_API_KEY || env.NOTUR_PUSH_KEY || env.NOTUR_API_KEY;
111
+ check('remote host configured', Boolean(host), host ? new URL(host).origin : '');
112
+ check('remote push key configured', Boolean(key));
113
+ }
114
+
115
+ if (!ok) {
116
+ console.error('Notur doctor found blocking issues.');
117
+ process.exit(1);
118
+ }
119
+
120
+ console.log('Notur doctor completed.');
121
+ }
122
+
123
+ main();
package/bin/notur-pack.js CHANGED
@@ -257,13 +257,19 @@ async function pack(sourceDir, outputPath, options = {}) {
257
257
  ];
258
258
 
259
259
  if (options.dryRun) {
260
- console.log('\nDry run mode (no archive written).');
261
- console.log(`Would create: ${outputFullPath}`);
262
- console.log(`Would include: ${archiveEntries.length} files`);
263
- if (deterministicArgs.length > 0) {
264
- console.log('Deterministic archive mode: enabled (GNU tar flags).');
265
- } else {
266
- console.log('Deterministic archive mode: not available (GNU tar not detected).');
260
+ try {
261
+ console.log('\nDry run mode (no archive written).');
262
+ console.log(`Would create: ${outputFullPath}`);
263
+ console.log(`Would include: ${archiveEntries.length} files`);
264
+ if (deterministicArgs.length > 0) {
265
+ console.log('Deterministic archive mode: enabled (GNU tar flags).');
266
+ } else {
267
+ console.log('Deterministic archive mode: not available (GNU tar not detected).');
268
+ }
269
+ } finally {
270
+ if (!checksumsExisted && fs.existsSync(checksumsPath)) {
271
+ fs.unlinkSync(checksumsPath);
272
+ }
267
273
  }
268
274
  return;
269
275
  }
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const yaml = require('yaml');
6
+
7
+ function parseArgs() {
8
+ const args = process.argv.slice(2);
9
+ const options = {
10
+ path: '.',
11
+ forceWebpack: false,
12
+ };
13
+
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i];
16
+ if (arg === '--force-webpack') {
17
+ options.forceWebpack = true;
18
+ } else if (arg === '--help' || arg === '-h') {
19
+ usage(0);
20
+ } else if (!arg.startsWith('-')) {
21
+ options.path = arg;
22
+ } else {
23
+ console.error(`Unknown argument: ${arg}`);
24
+ usage(1);
25
+ }
26
+ }
27
+
28
+ return options;
29
+ }
30
+
31
+ function usage(code) {
32
+ console.log(`Usage:
33
+ npx notur-sync [path]
34
+ npx @notur/sdk sync [path]
35
+
36
+ Options:
37
+ --force-webpack Regenerate webpack.config.js from extension.yaml`);
38
+ process.exit(code);
39
+ }
40
+
41
+ function readManifest(extensionPath) {
42
+ const manifestPath = ['extension.yaml', 'extension.yml']
43
+ .map(file => path.join(extensionPath, file))
44
+ .find(file => fs.existsSync(file));
45
+
46
+ if (!manifestPath) {
47
+ console.error(`Error: extension.yaml not found in ${extensionPath}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ const manifest = yaml.parse(fs.readFileSync(manifestPath, 'utf8'));
52
+ if (!manifest?.id || !manifest?.version) {
53
+ console.error('Error: extension.yaml must contain id and version.');
54
+ process.exit(1);
55
+ }
56
+
57
+ return manifest;
58
+ }
59
+
60
+ function packageName(id) {
61
+ return id.replace('/', '-');
62
+ }
63
+
64
+ function studly(value) {
65
+ return value
66
+ .split('-')
67
+ .filter(Boolean)
68
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
69
+ .join('');
70
+ }
71
+
72
+ function libraryName(id) {
73
+ return id
74
+ .split(/[\/-]/)
75
+ .filter(Boolean)
76
+ .map(studly)
77
+ .join('');
78
+ }
79
+
80
+ function bundlePath(manifest) {
81
+ return manifest?.frontend?.bundle || 'resources/frontend/dist/extension.js';
82
+ }
83
+
84
+ function syncPackageJson(extensionPath, manifest) {
85
+ const packagePath = path.join(extensionPath, 'package.json');
86
+ const current = fs.existsSync(packagePath)
87
+ ? JSON.parse(fs.readFileSync(packagePath, 'utf8'))
88
+ : {};
89
+
90
+ const next = {
91
+ ...current,
92
+ name: packageName(manifest.id),
93
+ version: manifest.version,
94
+ private: current.private ?? true,
95
+ scripts: {
96
+ ...(current.scripts || {}),
97
+ build: current.scripts?.build || 'webpack-cli --mode production --config webpack.config.js',
98
+ dev: current.scripts?.dev || 'webpack-cli --mode development --watch --config webpack.config.js',
99
+ pack: current.scripts?.pack || 'notur-pack',
100
+ push: current.scripts?.push || 'notur-push',
101
+ sync: current.scripts?.sync || 'notur-sync',
102
+ validate: current.scripts?.validate || 'notur-validate',
103
+ doctor: current.scripts?.doctor || 'notur-doctor',
104
+ },
105
+ peerDependencies: {
106
+ react: '^16.14.0',
107
+ 'react-dom': '^16.14.0',
108
+ ...(current.peerDependencies || {}),
109
+ },
110
+ devDependencies: {
111
+ '@notur/sdk': '^1.4.7',
112
+ '@types/react': '^16.14.0',
113
+ '@types/react-dom': '^16.9.0',
114
+ react: '^16.14.0',
115
+ 'react-dom': '^16.14.0',
116
+ 'ts-loader': '^9.5.0',
117
+ typescript: '^5.3.0',
118
+ webpack: '^5.90.0',
119
+ 'webpack-cli': '^6.0.0',
120
+ ...(current.devDependencies || {}),
121
+ },
122
+ };
123
+
124
+ fs.writeFileSync(packagePath, JSON.stringify(next, null, 2) + '\n');
125
+ console.log(` synced ${path.relative(process.cwd(), packagePath)}`);
126
+ }
127
+
128
+ function webpackTemplate(manifest) {
129
+ const bundle = bundlePath(manifest);
130
+ const filename = path.basename(bundle);
131
+ const outputDir = path.dirname(bundle);
132
+
133
+ return `const path = require('path');
134
+ const base = require('@notur/sdk/webpack.extension.config');
135
+
136
+ module.exports = {
137
+ ...base,
138
+ entry: './resources/frontend/src/index.tsx',
139
+ output: {
140
+ ...base.output,
141
+ filename: '${filename}',
142
+ path: path.resolve(__dirname, '${outputDir.replace(/\\/g, '/')}'),
143
+ library: {
144
+ ...base.output.library,
145
+ name: '__NOTUR_EXT_${libraryName(manifest.id)}__',
146
+ type: 'umd',
147
+ },
148
+ },
149
+ };
150
+ `;
151
+ }
152
+
153
+ function syncWebpack(extensionPath, manifest, force) {
154
+ if (!manifest.frontend) {
155
+ return;
156
+ }
157
+
158
+ const webpackPath = path.join(extensionPath, 'webpack.config.js');
159
+ if (fs.existsSync(webpackPath) && !force) {
160
+ return;
161
+ }
162
+
163
+ fs.writeFileSync(webpackPath, webpackTemplate(manifest));
164
+ console.log(` synced ${path.relative(process.cwd(), webpackPath)}`);
165
+ }
166
+
167
+ function syncEnvExample(extensionPath) {
168
+ const envExamplePath = path.join(extensionPath, '.env.example');
169
+ if (fs.existsSync(envExamplePath)) {
170
+ return;
171
+ }
172
+
173
+ fs.writeFileSync(envExamplePath, `NOTUR_HOST=https://panel.example.com
174
+ NOTUR_PUSH_KEY=notur_xxx
175
+ `);
176
+ console.log(` synced ${path.relative(process.cwd(), envExamplePath)}`);
177
+ }
178
+
179
+ function main() {
180
+ const options = parseArgs();
181
+ const extensionPath = path.resolve(options.path);
182
+ if (!fs.existsSync(extensionPath) || !fs.statSync(extensionPath).isDirectory()) {
183
+ console.error(`Error: extension path does not exist: ${extensionPath}`);
184
+ process.exit(1);
185
+ }
186
+
187
+ const manifest = readManifest(extensionPath);
188
+ console.log(`Syncing ${manifest.id} from extension.yaml`);
189
+
190
+ if (manifest.frontend) {
191
+ syncPackageJson(extensionPath, manifest);
192
+ syncWebpack(extensionPath, manifest, options.forceWebpack);
193
+ syncEnvExample(extensionPath);
194
+ }
195
+
196
+ console.log('Done.');
197
+ }
198
+
199
+ main();
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const yaml = require('yaml');
6
+
7
+ const KNOWN_SLOTS = new Set([
8
+ 'navbar', 'navbar.left', 'navbar.before', 'navbar.after',
9
+ 'server.subnav', 'server.subnav.before', 'server.subnav.after', 'server.header', 'server.page', 'server.footer',
10
+ 'server.terminal.buttons', 'server.console.header', 'server.console.info.before', 'server.console.info.after',
11
+ 'server.console.sidebar', 'server.console.command', 'server.console.footer', 'server.files.actions',
12
+ 'server.files.header', 'server.files.footer', 'server.files.dropdown', 'server.files.edit.before',
13
+ 'server.files.edit.after', 'server.databases.before', 'server.databases.after', 'server.schedules.before',
14
+ 'server.schedules.after', 'server.schedules.edit.before', 'server.schedules.edit.after', 'server.users.before',
15
+ 'server.users.after', 'server.backups.before', 'server.backups.after', 'server.backups.dropdown',
16
+ 'server.network.before', 'server.network.after', 'server.startup.before', 'server.startup.after',
17
+ 'server.settings.before', 'server.settings.after', 'dashboard.header', 'dashboard.widgets',
18
+ 'dashboard.serverlist.before', 'dashboard.serverlist.after', 'dashboard.serverrow.name.before',
19
+ 'dashboard.serverrow.name.after', 'dashboard.serverrow.description.before', 'dashboard.serverrow.description.after',
20
+ 'dashboard.serverrow.limits', 'dashboard.footer', 'dashboard.page', 'account.header', 'account.page',
21
+ 'account.footer', 'account.subnav', 'account.subnav.before', 'account.subnav.after',
22
+ 'account.overview.before', 'account.overview.after', 'account.api.before', 'account.api.after',
23
+ 'account.ssh.before', 'account.ssh.after', 'auth.container.before', 'auth.container.after',
24
+ ]);
25
+
26
+ function parseArgs() {
27
+ const args = process.argv.slice(2);
28
+ const options = { path: '.', strict: false };
29
+ for (let i = 0; i < args.length; i++) {
30
+ const arg = args[i];
31
+ if (arg === '--strict') {
32
+ options.strict = true;
33
+ } else if (arg === '--help' || arg === '-h') {
34
+ usage(0);
35
+ } else if (!arg.startsWith('-')) {
36
+ options.path = arg;
37
+ } else {
38
+ console.error(`Unknown argument: ${arg}`);
39
+ usage(1);
40
+ }
41
+ }
42
+ return options;
43
+ }
44
+
45
+ function usage(code) {
46
+ console.log(`Usage:
47
+ npx notur-validate [path]
48
+ npx @notur/sdk validate [path]
49
+
50
+ Options:
51
+ --strict Treat warnings as errors`);
52
+ process.exit(code);
53
+ }
54
+
55
+ function readManifest(basePath, result) {
56
+ const manifestPath = ['extension.yaml', 'extension.yml']
57
+ .map(file => path.join(basePath, file))
58
+ .find(file => fs.existsSync(file));
59
+ if (!manifestPath) {
60
+ result.errors.push('extension.yaml not found.');
61
+ return null;
62
+ }
63
+
64
+ try {
65
+ return yaml.parse(fs.readFileSync(manifestPath, 'utf8'));
66
+ } catch (error) {
67
+ result.errors.push(`extension.yaml could not be parsed: ${error.message}`);
68
+ return null;
69
+ }
70
+ }
71
+
72
+ function validateManifest(basePath, manifest, result) {
73
+ if (!manifest) return;
74
+
75
+ for (const field of ['id', 'name', 'version']) {
76
+ if (!manifest[field]) {
77
+ result.errors.push(`extension.yaml is missing required field: ${field}`);
78
+ }
79
+ }
80
+
81
+ if (manifest.id && !/^[a-z0-9-]+\/[a-z0-9-]+$/.test(manifest.id)) {
82
+ result.errors.push(`extension id "${manifest.id}" must use vendor/name with lowercase letters, numbers, and hyphens.`);
83
+ }
84
+
85
+ if (manifest.frontend?.bundle) {
86
+ const bundlePath = path.join(basePath, manifest.frontend.bundle);
87
+ if (!fs.existsSync(bundlePath)) {
88
+ result.warnings.push(`frontend.bundle does not exist yet: ${manifest.frontend.bundle}. Run your build before packaging.`);
89
+ }
90
+ }
91
+
92
+ const routes = manifest.backend?.routes;
93
+ if (routes && typeof routes === 'object') {
94
+ for (const [group, routeFile] of Object.entries(routes)) {
95
+ if (typeof routeFile !== 'string') {
96
+ result.errors.push(`backend.routes.${group} must be a string path.`);
97
+ continue;
98
+ }
99
+ if (!fs.existsSync(path.join(basePath, routeFile))) {
100
+ result.errors.push(`backend.routes.${group} points to missing file: ${routeFile}`);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ function readPackage(basePath, result) {
107
+ const packagePath = path.join(basePath, 'package.json');
108
+ if (!fs.existsSync(packagePath)) {
109
+ return null;
110
+ }
111
+ try {
112
+ return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
113
+ } catch (error) {
114
+ result.errors.push(`package.json could not be parsed: ${error.message}`);
115
+ return null;
116
+ }
117
+ }
118
+
119
+ function validatePackage(manifest, pkg, result) {
120
+ if (!manifest || !pkg) return;
121
+
122
+ const expectedName = manifest.id?.replace('/', '-');
123
+ if (expectedName && pkg.name && pkg.name !== expectedName) {
124
+ result.warnings.push(`package.json name "${pkg.name}" differs from manifest-derived "${expectedName}". Run npx notur-sync.`);
125
+ }
126
+
127
+ if (manifest.version && pkg.version && pkg.version !== manifest.version) {
128
+ result.warnings.push(`package.json version "${pkg.version}" differs from extension.yaml version "${manifest.version}". Run npx notur-sync.`);
129
+ }
130
+
131
+ for (const script of ['build', 'pack', 'push', 'sync']) {
132
+ if (!pkg.scripts?.[script]) {
133
+ result.warnings.push(`package.json is missing "${script}" script.`);
134
+ }
135
+ }
136
+ }
137
+
138
+ function validateFrontendSource(basePath, manifest, result) {
139
+ const indexPath = path.join(basePath, 'resources/frontend/src/index.tsx');
140
+ if (!fs.existsSync(indexPath)) {
141
+ if (manifest?.frontend) {
142
+ result.warnings.push('frontend is configured but resources/frontend/src/index.tsx was not found.');
143
+ }
144
+ return;
145
+ }
146
+
147
+ const source = fs.readFileSync(indexPath, 'utf8');
148
+ if (!source.includes('createExtension')) {
149
+ result.warnings.push('frontend entry does not appear to call createExtension().');
150
+ }
151
+
152
+ if (manifest?.id && !source.includes(manifest.id)) {
153
+ result.warnings.push(`frontend entry does not appear to reference manifest id "${manifest.id}".`);
154
+ }
155
+
156
+ const slotMatches = source.matchAll(/slot:\s*['"`]([^'"`]+)['"`]/g);
157
+ for (const match of slotMatches) {
158
+ if (!KNOWN_SLOTS.has(match[1])) {
159
+ result.warnings.push(`unknown slot id "${match[1]}". Check docs/slot-reference.md or SDK SlotId.`);
160
+ }
161
+ }
162
+ }
163
+
164
+ function printResult(result, strict) {
165
+ for (const warning of result.warnings) {
166
+ console.warn(`warning: ${warning}`);
167
+ }
168
+ for (const error of result.errors) {
169
+ console.error(`error: ${error}`);
170
+ }
171
+
172
+ const failed = result.errors.length > 0 || (strict && result.warnings.length > 0);
173
+ if (failed) {
174
+ console.error(`Notur validation failed (${result.errors.length} errors, ${result.warnings.length} warnings).`);
175
+ process.exit(1);
176
+ }
177
+
178
+ console.log(`Notur validation passed (${result.warnings.length} warnings).`);
179
+ }
180
+
181
+ function main() {
182
+ const options = parseArgs();
183
+ const basePath = path.resolve(options.path);
184
+ const result = { errors: [], warnings: [] };
185
+
186
+ if (!fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory()) {
187
+ result.errors.push(`path does not exist or is not a directory: ${basePath}`);
188
+ printResult(result, options.strict);
189
+ return;
190
+ }
191
+
192
+ const manifest = readManifest(basePath, result);
193
+ validateManifest(basePath, manifest, result);
194
+ const pkg = readPackage(basePath, result);
195
+ validatePackage(manifest, pkg, result);
196
+ validateFrontendSource(basePath, manifest, result);
197
+ printResult(result, options.strict);
198
+ }
199
+
200
+ main();