@notur/sdk 1.4.5 → 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 (51) hide show
  1. package/README.md +136 -1
  2. package/bin/notur-create.js +586 -0
  3. package/bin/notur-doctor.js +123 -0
  4. package/bin/notur-pack.js +13 -7
  5. package/bin/notur-push.js +331 -0
  6. package/bin/notur-sync.js +199 -0
  7. package/bin/notur-validate.js +200 -0
  8. package/bin/notur.js +46 -0
  9. package/dist/events.d.ts +10 -0
  10. package/dist/events.d.ts.map +1 -1
  11. package/dist/events.js +7 -0
  12. package/dist/events.js.map +1 -1
  13. package/dist/hooks/useExtensionConfig.d.ts +23 -4
  14. package/dist/hooks/useExtensionConfig.d.ts.map +1 -1
  15. package/dist/hooks/useExtensionConfig.js +8 -1
  16. package/dist/hooks/useExtensionConfig.js.map +1 -1
  17. package/dist/hooks/useNavigate.d.ts +27 -6
  18. package/dist/hooks/useNavigate.d.ts.map +1 -1
  19. package/dist/hooks/useNavigate.js +11 -2
  20. package/dist/hooks/useNavigate.js.map +1 -1
  21. package/dist/hooks/useNoturEvent.d.ts +12 -0
  22. package/dist/hooks/useNoturEvent.d.ts.map +1 -1
  23. package/dist/hooks/useNoturEvent.js +12 -0
  24. package/dist/hooks/useNoturEvent.js.map +1 -1
  25. package/dist/hooks/usePermission.d.ts +10 -1
  26. package/dist/hooks/usePermission.d.ts.map +1 -1
  27. package/dist/hooks/usePermission.js +10 -1
  28. package/dist/hooks/usePermission.js.map +1 -1
  29. package/dist/hooks/useServerContext.d.ts +18 -3
  30. package/dist/hooks/useServerContext.d.ts.map +1 -1
  31. package/dist/hooks/useServerContext.js +8 -1
  32. package/dist/hooks/useServerContext.js.map +1 -1
  33. package/dist/hooks/useUserContext.d.ts +10 -2
  34. package/dist/hooks/useUserContext.d.ts.map +1 -1
  35. package/dist/hooks/useUserContext.js +2 -0
  36. package/dist/hooks/useUserContext.js.map +1 -1
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/types.d.ts +179 -20
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js +6 -1
  43. package/dist/types.js.map +1 -1
  44. package/examples/red-button/.env.example +2 -0
  45. package/examples/red-button/README.md +30 -0
  46. package/examples/red-button/extension.yaml +9 -0
  47. package/examples/red-button/package.json +29 -0
  48. package/examples/red-button/resources/frontend/src/index.tsx +32 -0
  49. package/examples/red-button/tsconfig.json +13 -0
  50. package/examples/red-button/webpack.config.js +17 -0
  51. package/package.json +11 -3
@@ -0,0 +1,586 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ function parseArgs() {
9
+ const args = process.argv.slice(2);
10
+ const options = {
11
+ id: null,
12
+ path: process.cwd(),
13
+ preset: null,
14
+ displayName: null,
15
+ description: null,
16
+ withFrontend: true,
17
+ withApiRoutes: false,
18
+ force: false,
19
+ slot: 'dashboard.widgets',
20
+ packageManager: null,
21
+ install: null,
22
+ createEnv: null,
23
+ };
24
+
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i];
27
+ if (arg === '--path') {
28
+ options.path = args[++i];
29
+ } else if (arg === '--preset') {
30
+ options.preset = args[++i];
31
+ } else if (arg === '--name' || arg === '--display-name') {
32
+ options.displayName = args[++i];
33
+ } else if (arg === '--description') {
34
+ options.description = args[++i];
35
+ } else if (arg === '--with-frontend') {
36
+ options.withFrontend = true;
37
+ } else if (arg === '--no-frontend') {
38
+ options.withFrontend = false;
39
+ } else if (arg === '--with-api-routes') {
40
+ options.withApiRoutes = true;
41
+ } else if (arg === '--no-api-routes') {
42
+ options.withApiRoutes = false;
43
+ } else if (arg === '--force') {
44
+ options.force = true;
45
+ } else if (arg === '--slot') {
46
+ options.slot = args[++i];
47
+ } else if (arg === '--package-manager') {
48
+ options.packageManager = args[++i];
49
+ } else if (arg === '--install') {
50
+ options.install = true;
51
+ } else if (arg === '--no-install') {
52
+ options.install = false;
53
+ } else if (arg === '--env') {
54
+ options.createEnv = true;
55
+ } else if (arg === '--no-env') {
56
+ options.createEnv = false;
57
+ } else if (arg === '--help' || arg === '-h') {
58
+ usage(0);
59
+ } else if (!arg.startsWith('-') && !options.id) {
60
+ options.id = arg;
61
+ } else {
62
+ console.error(`Unknown argument: ${arg}`);
63
+ usage(1);
64
+ }
65
+ }
66
+
67
+ if (options.preset && !['frontend', 'backend', 'full', 'minimal'].includes(options.preset)) {
68
+ console.error('Error: preset must be one of frontend, backend, full, or minimal.');
69
+ process.exit(1);
70
+ }
71
+
72
+ if (options.packageManager && !['npm', 'pnpm', 'yarn', 'bun'].includes(options.packageManager)) {
73
+ console.error('Error: package manager must be one of npm, pnpm, yarn, or bun.');
74
+ process.exit(1);
75
+ }
76
+
77
+ return options;
78
+ }
79
+
80
+ function usage(code) {
81
+ console.log(`Usage:
82
+ npx notur-create acme/red-button [options]
83
+ npx @notur/sdk create acme/red-button [options]
84
+
85
+ Options:
86
+ --path <dir> Parent directory for the generated extension
87
+ --preset <name> frontend, backend, full, or minimal
88
+ --name <name> Display name for extension.yaml
89
+ --description <txt> Description for extension.yaml
90
+ --slot <slot> Initial frontend slot (default: dashboard.widgets)
91
+ --package-manager npm, pnpm, yarn, or bun
92
+ --install Install frontend dependencies after scaffolding
93
+ --env Create .env from .env.example
94
+ --no-frontend Generate PHP/manifest only
95
+ --with-api-routes Include a client API route stub
96
+ --force Allow writing into an existing empty directory`);
97
+ process.exit(code);
98
+ }
99
+
100
+ function isInteractive() {
101
+ return process.stdin.isTTY && process.stdout.isTTY;
102
+ }
103
+
104
+ function prompt(question, defaultValue = '') {
105
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
106
+ const rl = readline.createInterface({
107
+ input: process.stdin,
108
+ output: process.stdout,
109
+ });
110
+
111
+ return new Promise(resolve => {
112
+ rl.question(`${question}${suffix}: `, answer => {
113
+ rl.close();
114
+ resolve(answer.trim() || defaultValue);
115
+ });
116
+ });
117
+ }
118
+
119
+ async function select(question, choices, defaultChoice) {
120
+ const labels = choices.map(choice => choice.value).join('/');
121
+ while (true) {
122
+ const answer = await prompt(`${question} [${labels}]`, defaultChoice);
123
+ const match = choices.find(choice => choice.value === answer);
124
+ if (match) {
125
+ return match.value;
126
+ }
127
+ console.log(`Choose one of: ${labels}`);
128
+ }
129
+ }
130
+
131
+ async function confirm(question, defaultValue = false) {
132
+ const answer = await prompt(`${question} [${defaultValue ? 'Y/n' : 'y/N'}]`, defaultValue ? 'y' : 'n');
133
+ return ['y', 'yes'].includes(answer.toLowerCase());
134
+ }
135
+
136
+ function validateId(id) {
137
+ return /^[a-z0-9-]+\/[a-z0-9-]+$/.test(id);
138
+ }
139
+
140
+ function applyPreset(options) {
141
+ const preset = options.preset || 'frontend';
142
+ const features = {
143
+ frontend: true,
144
+ apiRoutes: false,
145
+ };
146
+
147
+ if (preset === 'minimal') {
148
+ features.frontend = false;
149
+ features.apiRoutes = false;
150
+ } else if (preset === 'backend') {
151
+ features.frontend = false;
152
+ features.apiRoutes = true;
153
+ } else if (preset === 'full') {
154
+ features.frontend = true;
155
+ features.apiRoutes = true;
156
+ }
157
+
158
+ if (options.withFrontend === false) {
159
+ features.frontend = false;
160
+ } else if (options.withFrontend === true && options.preset === null) {
161
+ features.frontend = true;
162
+ }
163
+
164
+ if (options.withApiRoutes === true) {
165
+ features.apiRoutes = true;
166
+ } else if (options.withApiRoutes === false && options.preset === null) {
167
+ features.apiRoutes = false;
168
+ }
169
+
170
+ options.withFrontend = features.frontend;
171
+ options.withApiRoutes = features.apiRoutes;
172
+ options.preset = preset;
173
+
174
+ return options;
175
+ }
176
+
177
+ async function resolveOptions(options) {
178
+ if (!options.id && !isInteractive()) {
179
+ console.error('Error: extension id is required in non-interactive mode.');
180
+ usage(1);
181
+ }
182
+
183
+ if (options.id && !validateId(options.id)) {
184
+ console.error('Error: extension id must use vendor/name format with lowercase letters, numbers, and hyphens.');
185
+ process.exit(1);
186
+ }
187
+
188
+ if (!options.id) {
189
+ console.log('Notur extension setup');
190
+ while (!options.id) {
191
+ const id = await prompt('Extension id', 'acme/red-button');
192
+ if (validateId(id)) {
193
+ options.id = id;
194
+ } else {
195
+ console.log('Use vendor/name format with lowercase letters, numbers, and hyphens.');
196
+ }
197
+ }
198
+ }
199
+
200
+ const [, name] = options.id.split('/');
201
+ if (!options.displayName && isInteractive()) {
202
+ options.displayName = await prompt('Display name', displayName(name));
203
+ }
204
+ if (!options.description && isInteractive()) {
205
+ options.description = await prompt('Description', 'A Notur extension');
206
+ }
207
+ if (!options.preset && isInteractive()) {
208
+ options.preset = await select('Preset', [
209
+ { value: 'frontend' },
210
+ { value: 'backend' },
211
+ { value: 'full' },
212
+ { value: 'minimal' },
213
+ ], 'frontend');
214
+ }
215
+ if (isInteractive() && ['frontend', 'full'].includes(options.preset) && options.withFrontend !== false) {
216
+ options.slot = await prompt('Initial frontend slot', options.slot);
217
+ }
218
+ if (!options.packageManager && isInteractive() && options.withFrontend !== false && options.preset !== 'minimal' && options.preset !== 'backend') {
219
+ options.packageManager = await select('Package manager', [
220
+ { value: 'npm' },
221
+ { value: 'pnpm' },
222
+ { value: 'yarn' },
223
+ { value: 'bun' },
224
+ ], 'npm');
225
+ }
226
+ if (options.createEnv === null && isInteractive()) {
227
+ options.createEnv = await confirm('Create .env from .env.example?', false);
228
+ }
229
+ if (options.install === null && isInteractive() && options.withFrontend !== false && options.preset !== 'minimal' && options.preset !== 'backend') {
230
+ options.install = await confirm('Install frontend dependencies now?', false);
231
+ }
232
+
233
+ return applyPreset(options);
234
+ }
235
+
236
+ function studly(value) {
237
+ return value
238
+ .split('-')
239
+ .filter(Boolean)
240
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
241
+ .join('');
242
+ }
243
+
244
+ function displayName(value) {
245
+ return value
246
+ .split('-')
247
+ .filter(Boolean)
248
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
249
+ .join(' ');
250
+ }
251
+
252
+ function libraryName(id) {
253
+ return id
254
+ .split(/[\/-]/)
255
+ .filter(Boolean)
256
+ .map(studly)
257
+ .join('');
258
+ }
259
+
260
+ function yamlString(value) {
261
+ return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
262
+ }
263
+
264
+ function writeFile(filePath, content) {
265
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
266
+ fs.writeFileSync(filePath, content);
267
+ console.log(` created ${path.relative(process.cwd(), filePath)}`);
268
+ }
269
+
270
+ function ensureTarget(target, force) {
271
+ if (!fs.existsSync(target)) {
272
+ fs.mkdirSync(target, { recursive: true });
273
+ return;
274
+ }
275
+
276
+ const entries = fs.readdirSync(target);
277
+ if (entries.length > 0 || !force) {
278
+ console.error(`Error: target directory already exists: ${target}`);
279
+ console.error('Use --force only for an existing empty directory.');
280
+ process.exit(1);
281
+ }
282
+ }
283
+
284
+ function manifestTemplate({ id, className, namespace, frontend, apiRoutes, display, description, phpEntrypoint }) {
285
+ const frontendSection = frontend
286
+ ? `
287
+ frontend:
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/"
297
+ `
298
+ : '';
299
+ const backendSection = apiRoutes
300
+ ? `
301
+ backend:
302
+ routes:
303
+ api-client: "src/routes/api-client.php"
304
+ `
305
+ : '';
306
+
307
+ return `notur: "1.0"
308
+ id: "${id}"
309
+ name: "${yamlString(display)}"
310
+ version: "1.0.0"
311
+ description: "${yamlString(description)}"
312
+ license: "MIT"
313
+ ${phpSection}
314
+ ${backendSection}
315
+ ${frontendSection}`;
316
+ }
317
+
318
+ function phpTemplate({ namespace, className }) {
319
+ return `<?php
320
+
321
+ declare(strict_types=1);
322
+
323
+ namespace ${namespace};
324
+
325
+ use Notur\\Support\\NoturExtension;
326
+
327
+ class ${className} extends NoturExtension
328
+ {
329
+ }
330
+ `;
331
+ }
332
+
333
+ function apiRouteTemplate() {
334
+ return `<?php
335
+
336
+ use Illuminate\\Support\\Facades\\Route;
337
+
338
+ Route::get('/ping', function () {
339
+ return response()->json([
340
+ 'message' => 'pong',
341
+ ]);
342
+ });
343
+ `;
344
+ }
345
+
346
+ function frontendTemplate({ id, slot }) {
347
+ return `import * as React from 'react';
348
+ import { createExtension } from '@notur/sdk';
349
+
350
+ const ExampleButton: React.FC = () => {
351
+ return (
352
+ <button
353
+ style={{
354
+ background: '#dc2626',
355
+ color: '#fff',
356
+ border: 0,
357
+ borderRadius: '6px',
358
+ padding: '8px 12px',
359
+ fontWeight: 600,
360
+ cursor: 'pointer',
361
+ }}
362
+ onClick={() => alert('Hello from Notur')}
363
+ >
364
+ Red Button
365
+ </button>
366
+ );
367
+ };
368
+
369
+ createExtension({
370
+ id: '${id}',
371
+ slots: [
372
+ {
373
+ slot: '${slot}',
374
+ component: ExampleButton,
375
+ order: 10,
376
+ },
377
+ ],
378
+ });
379
+ `;
380
+ }
381
+
382
+ function packageTemplate(id) {
383
+ return `${JSON.stringify({
384
+ name: id.replace('/', '-'),
385
+ version: '1.0.0',
386
+ private: true,
387
+ scripts: {
388
+ build: 'webpack-cli --mode production --config webpack.config.js',
389
+ dev: 'webpack-cli --mode development --watch --config webpack.config.js',
390
+ pack: 'notur-pack',
391
+ push: 'notur-push',
392
+ sync: 'notur-sync',
393
+ validate: 'notur-validate',
394
+ doctor: 'notur-doctor',
395
+ },
396
+ peerDependencies: {
397
+ react: '^16.14.0',
398
+ 'react-dom': '^16.14.0',
399
+ },
400
+ devDependencies: {
401
+ '@notur/sdk': '^1.4.7',
402
+ '@types/react': '^16.14.0',
403
+ '@types/react-dom': '^16.9.0',
404
+ react: '^16.14.0',
405
+ 'react-dom': '^16.14.0',
406
+ 'ts-loader': '^9.5.0',
407
+ typescript: '^5.3.0',
408
+ webpack: '^5.90.0',
409
+ 'webpack-cli': '^6.0.0',
410
+ },
411
+ }, null, 2)}
412
+ `;
413
+ }
414
+
415
+ function tsconfigTemplate() {
416
+ return `{
417
+ "compilerOptions": {
418
+ "target": "ES2019",
419
+ "module": "ESNext",
420
+ "moduleResolution": "Node",
421
+ "jsx": "react",
422
+ "strict": true,
423
+ "esModuleInterop": true,
424
+ "skipLibCheck": true,
425
+ "forceConsistentCasingInFileNames": true
426
+ },
427
+ "include": ["resources/frontend/src/**/*"]
428
+ }
429
+ `;
430
+ }
431
+
432
+ function webpackTemplate(libName) {
433
+ return `const path = require('path');
434
+ const base = require('@notur/sdk/webpack.extension.config');
435
+
436
+ module.exports = {
437
+ ...base,
438
+ entry: './resources/frontend/src/index.tsx',
439
+ output: {
440
+ ...base.output,
441
+ filename: 'extension.js',
442
+ path: path.resolve(__dirname, 'resources/frontend/dist'),
443
+ library: {
444
+ ...base.output.library,
445
+ name: '__NOTUR_EXT_${libName}__',
446
+ type: 'umd',
447
+ },
448
+ },
449
+ };
450
+ `;
451
+ }
452
+
453
+ function readmeTemplate(id) {
454
+ return `# ${id}
455
+
456
+ Development:
457
+
458
+ \`\`\`bash
459
+ npm install
460
+ npm run build
461
+ npx notur-pack
462
+ \`\`\`
463
+
464
+ Remote push to a Notur-enabled panel:
465
+
466
+ \`\`\`bash
467
+ cp .env.example .env
468
+ npm run push
469
+ \`\`\`
470
+
471
+ Or pass values directly:
472
+
473
+ \`\`\`bash
474
+ npx notur-push --host https://panel.example.com --key notur_xxx
475
+ \`\`\`
476
+ `;
477
+ }
478
+
479
+ function installCommand(packageManager) {
480
+ if (packageManager === 'pnpm') return ['pnpm', ['install']];
481
+ if (packageManager === 'yarn') return ['yarn', ['install']];
482
+ if (packageManager === 'bun') return ['bun', ['install']];
483
+ return ['npm', ['install']];
484
+ }
485
+
486
+ function runScriptCommand(packageManager, script) {
487
+ if (packageManager === 'bun') return `bun run ${script}`;
488
+ if (packageManager === 'pnpm') return `pnpm run ${script}`;
489
+ if (packageManager === 'yarn') return `yarn ${script}`;
490
+ return `npm run ${script}`;
491
+ }
492
+
493
+ function runInstall(target, packageManager) {
494
+ const [command, args] = installCommand(packageManager || 'npm');
495
+ console.log(`\nRunning ${command} ${args.join(' ')}...`);
496
+ const result = spawnSync(command, args, {
497
+ cwd: target,
498
+ stdio: 'inherit',
499
+ });
500
+
501
+ if (result.status !== 0) {
502
+ console.warn(`Dependency install failed. Run ${command} ${args.join(' ')} manually in ${target}.`);
503
+ }
504
+ }
505
+
506
+ async function main() {
507
+ const options = await resolveOptions(parseArgs());
508
+ const [vendor, name] = options.id.split('/');
509
+ const namespace = `${studly(vendor)}\\${studly(name)}`;
510
+ const className = `${studly(name)}Extension`;
511
+ const target = path.resolve(options.path, name);
512
+ const needsPhpEntrypoint = options.withApiRoutes;
513
+
514
+ ensureTarget(target, options.force);
515
+
516
+ console.log(`Scaffolding ${options.id} in ${target}`);
517
+
518
+ writeFile(path.join(target, 'extension.yaml'), manifestTemplate({
519
+ id: options.id,
520
+ vendor,
521
+ name,
522
+ namespace,
523
+ className,
524
+ frontend: options.withFrontend,
525
+ apiRoutes: options.withApiRoutes,
526
+ display: options.displayName || displayName(name),
527
+ description: options.description || 'A Notur extension',
528
+ phpEntrypoint: needsPhpEntrypoint,
529
+ }));
530
+ if (needsPhpEntrypoint) {
531
+ writeFile(path.join(target, 'src', `${className}.php`), phpTemplate({ namespace, className }));
532
+ }
533
+ if (options.withApiRoutes) {
534
+ writeFile(path.join(target, 'src/routes/api-client.php'), apiRouteTemplate());
535
+ }
536
+ writeFile(path.join(target, 'README.md'), readmeTemplate(options.id));
537
+ writeFile(path.join(target, '.env.example'), `NOTUR_HOST=https://panel.example.com
538
+ NOTUR_PUSH_KEY=notur_xxx
539
+ `);
540
+ if (options.createEnv) {
541
+ writeFile(path.join(target, '.env'), `NOTUR_HOST=https://panel.example.com
542
+ NOTUR_PUSH_KEY=notur_xxx
543
+ `);
544
+ }
545
+ writeFile(path.join(target, '.gitignore'), `node_modules/
546
+ vendor/
547
+ resources/frontend/dist/
548
+ .env
549
+ *.notur
550
+ *.notur.sha256
551
+ *.notur.sig
552
+ `);
553
+
554
+ if (options.withFrontend) {
555
+ writeFile(path.join(target, 'resources/frontend/src/index.tsx'), frontendTemplate({
556
+ id: options.id,
557
+ slot: options.slot,
558
+ }));
559
+ writeFile(path.join(target, 'package.json'), packageTemplate(options.id));
560
+ writeFile(path.join(target, 'tsconfig.json'), tsconfigTemplate());
561
+ writeFile(path.join(target, 'webpack.config.js'), webpackTemplate(libraryName(options.id)));
562
+ }
563
+
564
+ if (options.install && options.withFrontend) {
565
+ runInstall(target, options.packageManager);
566
+ }
567
+
568
+ console.log('\nNext steps:');
569
+ if (options.withFrontend) {
570
+ console.log(` cd ${target}`);
571
+ if (!options.install) {
572
+ const [command, args] = installCommand(options.packageManager || 'npm');
573
+ console.log(` ${command} ${args.join(' ')}`);
574
+ }
575
+ console.log(` ${runScriptCommand(options.packageManager || 'npm', 'build')}`);
576
+ console.log(' npx notur-pack');
577
+ } else {
578
+ console.log(` cd ${target}`);
579
+ console.log(' npx notur-pack');
580
+ }
581
+ }
582
+
583
+ main().catch(error => {
584
+ console.error(error?.message || String(error));
585
+ process.exit(1);
586
+ });
@@ -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();