neutrinos-cli 2.0.0-beta.2 → 2.0.0-beta.4

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 (34) hide show
  1. package/README.md +23 -0
  2. package/dist/src/bin/cli.js +34 -1
  3. package/dist/src/commands/dev.js +12 -7
  4. package/dist/src/commands/doctor.js +69 -0
  5. package/dist/src/commands/new-workspace.js +9 -8
  6. package/dist/src/commands/test.js +27 -0
  7. package/dist/src/types/doctor.js +1 -0
  8. package/dist/src/utils/doctor-checks/auth-state.js +21 -0
  9. package/dist/src/utils/doctor-checks/component-entry-file.js +40 -0
  10. package/dist/src/utils/doctor-checks/get-packages-safe.js +16 -0
  11. package/dist/src/utils/doctor-checks/index.js +26 -0
  12. package/dist/src/utils/doctor-checks/lock-file-sync.js +38 -0
  13. package/dist/src/utils/doctor-checks/node-modules.js +17 -0
  14. package/dist/src/utils/doctor-checks/node-version.js +10 -0
  15. package/dist/src/utils/doctor-checks/package-alpha-block.js +62 -0
  16. package/dist/src/utils/doctor-checks/plugin-json.js +56 -0
  17. package/dist/src/utils/doctor-checks/plugins-server.js +21 -0
  18. package/dist/src/utils/doctor-checks/root-package-json.js +51 -0
  19. package/dist/src/utils/doctor-checks/storybook.js +26 -0
  20. package/dist/src/utils/doctor-checks/tsconfig.js +25 -0
  21. package/dist/src/utils/doctor-checks/vitest-config.js +16 -0
  22. package/dist/src/utils/doctor-checks.js +397 -0
  23. package/dist/src/utils/generate-component.js +3 -2
  24. package/dist/src/utils/local-cli.js +23 -0
  25. package/package.json +1 -1
  26. package/templates/component/.stories.ts.hbs +53 -0
  27. package/templates/project/.storybook/decorators/event-logger.ts +153 -0
  28. package/templates/project/.storybook/main.ts +19 -0
  29. package/templates/project/.storybook/preview.ts +12 -0
  30. package/templates/project/tsconfig.json +1 -1
  31. package/templates/project/vitest.config.ts +24 -0
  32. package/templates/component/.spec.ts.hbs +0 -15
  33. package/templates/project/index.html +0 -24
  34. package/templates/project/index.ts +0 -86
package/README.md CHANGED
@@ -40,6 +40,7 @@ neutrinos <command> [options]
40
40
  | `publish [name]` | Bundle and publish a package |
41
41
  | `deprecate [name]` | Deprecate a published package |
42
42
  | `auth` | Login or check auth state |
43
+ | `doctor` | Check workspace health (`--fix` to auto-repair) |
43
44
  | `completion <shell>` | Output shell completion script (`bash` or `zsh`) |
44
45
 
45
46
  ### `new <name>`
@@ -106,6 +107,28 @@ neutrinos deprecate [name] [-y] [--all]
106
107
 
107
108
  Marks a published package as deprecated. Prompts for confirmation unless `-y` is passed.
108
109
 
110
+ ### `doctor`
111
+
112
+ ```sh
113
+ neutrinos doctor # Run all health checks
114
+ neutrinos doctor --fix # Auto-fix what it can
115
+ ```
116
+
117
+ Runs 10 workspace health checks and reports pass/warn/fail for each:
118
+
119
+ - `node_modules` — Dependencies installed
120
+ - `plugin.json` — Exists with required fields (`name`, `components.selectorPrefix`, `modules.idPrefix`)
121
+ - `package.json` — Has `workspaces`, `type: "module"`, expected dependencies
122
+ - `tsconfig.json` — Exists and is valid (verified via `tsc --showConfig`)
123
+ - `plugins-server/` — Directory exists with `index.js`
124
+ - Package `alpha` block — Each package has `alpha.component` or `alpha.module`
125
+ - Component entry file — Component packages have a matching `<name>.ts` file
126
+ - Node version — Meets `>=22` requirement
127
+ - Auth state — Token file exists and is not expired
128
+ - Lock file sync — Lock file is up to date with `package.json`
129
+
130
+ `--fix` auto-repairs: missing `node_modules` (runs install), `plugin.json` defaults, `workspaces`/`type` in `package.json`, `plugins-server/` from template, missing `alpha` blocks, stale lock files.
131
+
109
132
  ### `auth`
110
133
 
111
134
  ```sh
@@ -4,6 +4,21 @@ if (major < 22) {
4
4
  console.error('neutrinos requires Node.js 22+. Current: ' + process.version);
5
5
  process.exit(1);
6
6
  }
7
+ // Delegate to workspace-local CLI binary if it exists and differs from this one.
8
+ // This ensures the workspace's pinned CLI version is used, not the global install.
9
+ import { resolveLocalCli } from '../utils/local-cli.js';
10
+ const localScript = resolveLocalCli(process.cwd(), realpathSync(fileURLToPath(import.meta.url)));
11
+ if (localScript) {
12
+ const child = spawn(process.execPath, [localScript, ...process.argv.slice(2)], {
13
+ stdio: 'inherit',
14
+ });
15
+ process.on('SIGINT', () => child.kill('SIGINT'));
16
+ process.on('SIGTERM', () => child.kill('SIGTERM'));
17
+ const code = await new Promise((resolve) => {
18
+ child.on('close', (c) => resolve(c ?? 1));
19
+ });
20
+ process.exit(code);
21
+ }
7
22
  import dotenv from 'dotenv';
8
23
  import { PACKAGE_ROOT } from '../utils/path-utils.js';
9
24
  dotenv.config({
@@ -14,6 +29,7 @@ import { Argument, Command, Option } from 'commander';
14
29
  import EventEmitter from 'node:events';
15
30
  import { cwd, env, exit } from 'node:process';
16
31
  import { readFileSync, realpathSync } from 'node:fs';
32
+ import { spawn } from 'node:child_process';
17
33
  import { fileURLToPath } from 'node:url';
18
34
  import open from 'open';
19
35
  import { join, resolve } from 'node:path';
@@ -26,7 +42,9 @@ import { generate } from '../commands/generate.js';
26
42
  import { createWorkspace } from '../commands/new-workspace.js';
27
43
  import { publish } from '../commands/publish.js';
28
44
  import { startPluginsServer } from '../commands/serve.js';
45
+ import { runTests } from '../commands/test.js';
29
46
  import { completion } from '../commands/completion.js';
47
+ import { doctor } from '../commands/doctor.js';
30
48
  import { getPackages } from '../utils/get-packages.js';
31
49
  import { validateWorkspace } from '../utils/check-valid-ws.js';
32
50
  import { done, failed, inprogress, log } from '../utils/logger.js';
@@ -178,6 +196,14 @@ export const createProgram = () => {
178
196
  .action((options) => {
179
197
  servePlugin(cwd(), Number(options.port) || 6969);
180
198
  });
199
+ program
200
+ .command('test')
201
+ .alias('t')
202
+ .option('-w, --watch', 'Run in watch mode')
203
+ .description('run story-based tests')
204
+ .action((options) => {
205
+ runTests(cwd(), options);
206
+ });
181
207
  program
182
208
  .command('serve')
183
209
  .option('-p, --port <port>', 'Port number for serving the plugins', '3000')
@@ -192,6 +218,13 @@ export const createProgram = () => {
192
218
  .action((shell) => {
193
219
  completion(program, shell);
194
220
  });
221
+ program
222
+ .command('doctor')
223
+ .description('Check workspace health')
224
+ .option('--fix', 'Auto-fix issues where possible')
225
+ .action((options) => {
226
+ doctor(cwd(), options);
227
+ });
195
228
  program
196
229
  .command('__list-packages', { hidden: true })
197
230
  .description('List workspace package names (used by shell completion)')
@@ -209,7 +242,7 @@ export const createProgram = () => {
209
242
  });
210
243
  program.hook('preAction', async (_thisCmd, actionCmd) => {
211
244
  const cmd = actionCmd.name();
212
- if (cmd === 'new' || cmd === 'login' || cmd === 'completion' || cmd === '__list-packages') {
245
+ if (cmd === 'new' || cmd === 'login' || cmd === 'completion' || cmd === '__list-packages' || cmd === 'doctor') {
213
246
  return;
214
247
  }
215
248
  if (!validateWorkspace(cwd())) {
@@ -1,10 +1,15 @@
1
- import { createServer } from 'vite';
1
+ import { spawn } from 'node:child_process';
2
2
  export const servePlugin = async (wsPath, port) => {
3
- const server = await createServer({
4
- root: wsPath,
5
- server: { port },
3
+ const child = spawn('npx', ['storybook', 'dev', '-p', String(port)], {
4
+ cwd: wsPath,
5
+ stdio: 'inherit',
6
+ });
7
+ await new Promise((resolve, reject) => {
8
+ child.on('close', (code) => {
9
+ if (code === 0)
10
+ resolve();
11
+ else
12
+ reject(new Error(`Storybook exited with code ${code}`));
13
+ });
6
14
  });
7
- await server.listen();
8
- server.printUrls();
9
- server.bindCLIShortcuts({ print: true });
10
15
  };
@@ -0,0 +1,69 @@
1
+ import { bold, greenBright, red, yellowBright } from 'colorette';
2
+ import { log as _log } from 'node:console';
3
+ import { checks } from '../utils/doctor-checks/index.js';
4
+ const SYMBOLS = {
5
+ pass: greenBright('✔'),
6
+ warn: yellowBright('⚠'),
7
+ fail: red('✖'),
8
+ };
9
+ const colorize = {
10
+ pass: greenBright,
11
+ warn: yellowBright,
12
+ fail: red,
13
+ };
14
+ function printResult(result) {
15
+ const sym = SYMBOLS[result.status];
16
+ const color = colorize[result.status];
17
+ _log(` ${sym} ${color(`${result.name} — ${result.message}`)}`);
18
+ }
19
+ function printFixedResult(before, after) {
20
+ _log(` ${red('✖')} → ${greenBright('✔')} ${greenBright(`${after.name} — Fixed: ${after.message}`)}`);
21
+ }
22
+ export function doctor(wsPath, opts) {
23
+ _log('\nWorkspace Health Check');
24
+ _log('──────────────────────\n');
25
+ let results = [];
26
+ for (const check of checks) {
27
+ const checkResults = check.run(wsPath);
28
+ results.push(...checkResults);
29
+ }
30
+ for (const r of results) {
31
+ printResult(r);
32
+ }
33
+ if (opts.fix) {
34
+ const fixableFailures = results.filter((r) => r.status === 'fail' && r.fixable);
35
+ if (fixableFailures.length > 0) {
36
+ _log('');
37
+ for (const check of checks) {
38
+ if (!check.fix)
39
+ continue;
40
+ const checkResults = check.run(wsPath);
41
+ const hasFixableFailure = checkResults.some((r) => r.status === 'fail' && r.fixable);
42
+ if (!hasFixableFailure)
43
+ continue;
44
+ check.fix(wsPath);
45
+ const afterResults = check.run(wsPath);
46
+ for (let i = 0; i < checkResults.length; i++) {
47
+ const before = checkResults[i];
48
+ const after = afterResults[i];
49
+ if (before.status === 'fail' && after && after.status === 'pass') {
50
+ printFixedResult(before, after);
51
+ const idx = results.findIndex((r) => r.name === before.name && r.status === 'fail');
52
+ if (idx !== -1) {
53
+ results[idx] = after;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ const passed = results.filter((r) => r.status === 'pass').length;
61
+ const warned = results.filter((r) => r.status === 'warn').length;
62
+ const failed = results.filter((r) => r.status === 'fail').length;
63
+ _log('\n──────────────────────');
64
+ _log(bold(`Summary: ${greenBright(`${passed} passed`)}, ${yellowBright(`${warned} warnings`)}, ${red(`${failed} failed`)}`));
65
+ _log('');
66
+ if (failed > 0) {
67
+ process.exit(1);
68
+ }
69
+ }
@@ -75,16 +75,19 @@ const cleanUp = (dir) => {
75
75
  done('Cleaned up workspace');
76
76
  };
77
77
  const initializePackages = async (dir) => {
78
- inprogress('Adding default npm scripts to workspace...');
78
+ inprogress('Installing default packages in workspace...');
79
79
  const pkgJson = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
80
- const scripts = {
81
- build: 'neutrinos build',
82
- start: 'neutrinos start',
83
- serve: 'neutrinos serve',
84
- };
80
+ pkgJson.scripts = {};
85
81
  pkgJson.devDependencies = {
82
+ 'neutrinos-cli': '^2.0.0',
86
83
  lit: '^3.3.2',
87
84
  typescript: '^5.9.3',
85
+ storybook: '^10.3.1',
86
+ '@storybook/web-components-vite': '^10.3.1',
87
+ '@storybook/addon-vitest': '^10.3.1',
88
+ vitest: '^4.0.0',
89
+ '@vitest/browser': '^4.0.0',
90
+ '@vitest/browser-playwright': '^4.0.0',
88
91
  };
89
92
  pkgJson.dependencies = {
90
93
  express: '^5.2.1',
@@ -92,13 +95,11 @@ const initializePackages = async (dir) => {
92
95
  };
93
96
  pkgJson.workspaces = ['packages/*'];
94
97
  pkgJson.type = 'module';
95
- Object.assign(pkgJson.scripts, {}, scripts);
96
98
  writeFileSync(join(dir, 'package.json'), await prettify(pkgJson, 'json'));
97
99
  execSync('npm install', {
98
100
  cwd: dir,
99
101
  });
100
102
  done(`Installed default packages in workspace: ${bold(dir)}`);
101
- done(`Added default npm scripts to workspace: ${bold(dir)}`);
102
103
  };
103
104
  const initializeStaticServer = (dir) => {
104
105
  cpSync(pluginServerTemplatesPath(), join(dir, 'plugins-server'), {
@@ -0,0 +1,27 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { failed } from '../utils/logger.js';
5
+ export const runTests = async (wsPath, options) => {
6
+ const vitestConfig = join(wsPath, 'vitest.config.ts');
7
+ if (!existsSync(vitestConfig)) {
8
+ failed('vitest.config.ts not found in workspace. Run "neutrinos doctor --fix" or create it manually.');
9
+ process.exit(1);
10
+ }
11
+ const args = ['vitest'];
12
+ if (!options.watch) {
13
+ args.push('--run');
14
+ }
15
+ const child = spawn('npx', args, {
16
+ cwd: wsPath,
17
+ stdio: 'inherit',
18
+ });
19
+ await new Promise((resolve, reject) => {
20
+ child.on('close', (code) => {
21
+ if (code === 0)
22
+ resolve();
23
+ else
24
+ reject(new Error(`Vitest exited with code ${code}`));
25
+ });
26
+ });
27
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { authConfigJson } from '../path-utils.js';
3
+ export const authStateCheck = {
4
+ name: 'Auth state',
5
+ run() {
6
+ const authPath = authConfigJson();
7
+ if (!existsSync(authPath)) {
8
+ return [{ name: 'Auth state', status: 'fail', message: `${authPath} not found. Run "neutrinos auth login"`, fixable: false }];
9
+ }
10
+ try {
11
+ const tokenSet = JSON.parse(readFileSync(authPath, 'utf-8'));
12
+ if (tokenSet.expires_at && tokenSet.expires_at < Date.now() / 1000) {
13
+ return [{ name: 'Auth state', status: 'warn', message: 'Token expired. Run "neutrinos auth login"', fixable: false }];
14
+ }
15
+ }
16
+ catch {
17
+ return [{ name: 'Auth state', status: 'warn', message: 'Could not parse auth.json', fixable: false }];
18
+ }
19
+ return [{ name: 'Auth state', status: 'pass', message: 'Token present and valid', fixable: false }];
20
+ },
21
+ };
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { getGeneratedComponentName } from '../path-utils.js';
4
+ import { getPackagesSafe } from './get-packages-safe.js';
5
+ export const componentEntryFileCheck = {
6
+ name: 'Component entry file',
7
+ run(wsPath) {
8
+ const packages = getPackagesSafe(wsPath);
9
+ if (packages === null) {
10
+ return [{ name: 'Component entry file', status: 'fail', message: 'Could not discover packages', fixable: false }];
11
+ }
12
+ const results = [];
13
+ for (const pkgPath of packages) {
14
+ const pkgJsonPath = join(pkgPath, 'package.json');
15
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
16
+ if (!pkgJson.alpha?.component)
17
+ continue;
18
+ const pkgName = pkgJson.name || basename(pkgPath);
19
+ const entryName = getGeneratedComponentName(basename(pkgPath));
20
+ const entryFile = join(pkgPath, `${entryName}.ts`);
21
+ if (!existsSync(entryFile)) {
22
+ results.push({
23
+ name: `Package "${pkgName}" entry file`,
24
+ status: 'fail',
25
+ message: `${entryName}.ts is missing`,
26
+ fixable: false,
27
+ });
28
+ }
29
+ else {
30
+ results.push({
31
+ name: `Package "${pkgName}" entry file`,
32
+ status: 'pass',
33
+ message: `${entryName}.ts exists`,
34
+ fixable: false,
35
+ });
36
+ }
37
+ }
38
+ return results;
39
+ },
40
+ };
@@ -0,0 +1,16 @@
1
+ import { existsSync, globSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export function getPackagesSafe(wsPath) {
4
+ try {
5
+ const packageJsonContent = readFileSync(join(wsPath, 'package.json'), 'utf-8');
6
+ const parsed = JSON.parse(packageJsonContent);
7
+ const packagesDir = parsed.workspaces ?? [];
8
+ const packageNames = globSync(packagesDir, { cwd: wsPath });
9
+ return packageNames
10
+ .map((name) => join(wsPath, name))
11
+ .filter((p) => existsSync(join(p, 'package.json')));
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
@@ -0,0 +1,26 @@
1
+ import { nodeModulesCheck } from './node-modules.js';
2
+ import { pluginJsonCheck } from './plugin-json.js';
3
+ import { rootPackageJsonCheck } from './root-package-json.js';
4
+ import { tsconfigCheck } from './tsconfig.js';
5
+ import { pluginsServerCheck } from './plugins-server.js';
6
+ import { storybookCheck } from './storybook.js';
7
+ import { vitestConfigCheck } from './vitest-config.js';
8
+ import { packageAlphaBlockCheck } from './package-alpha-block.js';
9
+ import { componentEntryFileCheck } from './component-entry-file.js';
10
+ import { nodeVersionCheck } from './node-version.js';
11
+ import { authStateCheck } from './auth-state.js';
12
+ import { lockFileSyncCheck } from './lock-file-sync.js';
13
+ export const checks = [
14
+ nodeModulesCheck,
15
+ pluginJsonCheck,
16
+ rootPackageJsonCheck,
17
+ tsconfigCheck,
18
+ pluginsServerCheck,
19
+ storybookCheck,
20
+ vitestConfigCheck,
21
+ packageAlphaBlockCheck,
22
+ componentEntryFileCheck,
23
+ nodeVersionCheck,
24
+ authStateCheck,
25
+ lockFileSyncCheck,
26
+ ];
@@ -0,0 +1,38 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export const lockFileSyncCheck = {
5
+ name: 'Lock file sync',
6
+ run(wsPath) {
7
+ const packageJsonPath = join(wsPath, 'package.json');
8
+ if (!existsSync(packageJsonPath)) {
9
+ return [{ name: 'Lock file sync', status: 'fail', message: 'No package.json to compare against', fixable: false }];
10
+ }
11
+ const pnpmLock = join(wsPath, 'pnpm-lock.yaml');
12
+ const npmLock = join(wsPath, 'package-lock.json');
13
+ let lockPath = null;
14
+ let manager = 'npm';
15
+ if (existsSync(pnpmLock)) {
16
+ lockPath = pnpmLock;
17
+ manager = 'pnpm';
18
+ }
19
+ else if (existsSync(npmLock)) {
20
+ lockPath = npmLock;
21
+ manager = 'npm';
22
+ }
23
+ if (!lockPath) {
24
+ return [{ name: 'Lock file sync', status: 'fail', message: 'No lock file found. Run "npm install" or "pnpm install"', fixable: true }];
25
+ }
26
+ const lockMtime = statSync(lockPath).mtimeMs;
27
+ const pkgMtime = statSync(packageJsonPath).mtimeMs;
28
+ if (pkgMtime > lockMtime) {
29
+ return [{ name: 'Lock file sync', status: 'warn', message: `package.json is newer than lock file. Run "${manager} install"`, fixable: true }];
30
+ }
31
+ return [{ name: 'Lock file sync', status: 'pass', message: 'Lock file is up to date', fixable: false }];
32
+ },
33
+ fix(wsPath) {
34
+ const pnpmLock = join(wsPath, 'pnpm-lock.yaml');
35
+ const manager = existsSync(pnpmLock) ? 'pnpm' : 'npm';
36
+ execSync(`${manager} install`, { cwd: wsPath, stdio: 'inherit' });
37
+ },
38
+ };
@@ -0,0 +1,17 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export const nodeModulesCheck = {
5
+ name: 'node_modules',
6
+ run(wsPath) {
7
+ if (!existsSync(join(wsPath, 'node_modules'))) {
8
+ return [{ name: 'node_modules', status: 'fail', message: 'Not installed. Run "npm install" or "pnpm install"', fixable: true }];
9
+ }
10
+ return [{ name: 'node_modules', status: 'pass', message: 'Dependencies installed', fixable: false }];
11
+ },
12
+ fix(wsPath) {
13
+ const pnpmLock = join(wsPath, 'pnpm-lock.yaml');
14
+ const manager = existsSync(pnpmLock) ? 'pnpm' : 'npm';
15
+ execSync(`${manager} install`, { cwd: wsPath, stdio: 'inherit' });
16
+ },
17
+ };
@@ -0,0 +1,10 @@
1
+ export const nodeVersionCheck = {
2
+ name: 'Node version',
3
+ run() {
4
+ const [major] = process.versions.node.split('.').map(Number);
5
+ if (major < 22) {
6
+ return [{ name: 'Node version', status: 'fail', message: `v${process.versions.node} does not meet requirement (>=22). Install Node.js 22+`, fixable: false }];
7
+ }
8
+ return [{ name: 'Node version', status: 'pass', message: `v${process.versions.node} meets requirement (>=22)`, fixable: false }];
9
+ },
10
+ };
@@ -0,0 +1,62 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { getPackagesSafe } from './get-packages-safe.js';
4
+ export const packageAlphaBlockCheck = {
5
+ name: 'Package alpha block',
6
+ run(wsPath) {
7
+ const packages = getPackagesSafe(wsPath);
8
+ if (packages === null) {
9
+ return [{ name: 'Package alpha block', status: 'fail', message: 'Could not discover packages (missing package.json or workspaces)', fixable: false }];
10
+ }
11
+ if (packages.length === 0) {
12
+ return [{ name: 'Package alpha block', status: 'pass', message: 'No packages found (nothing to check)', fixable: false }];
13
+ }
14
+ const results = [];
15
+ for (const pkgPath of packages) {
16
+ const pkgJsonPath = join(pkgPath, 'package.json');
17
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
18
+ const pkgName = pkgJson.name || basename(pkgPath);
19
+ if (!pkgJson.alpha) {
20
+ results.push({
21
+ name: `Package "${pkgName}" alpha block`,
22
+ status: 'fail',
23
+ message: 'No alpha block found. Add alpha.component or alpha.module',
24
+ fixable: true,
25
+ });
26
+ }
27
+ else if (!pkgJson.alpha.component && !pkgJson.alpha.module) {
28
+ results.push({
29
+ name: `Package "${pkgName}" alpha block`,
30
+ status: 'warn',
31
+ message: 'alpha block exists but neither component nor module is true',
32
+ fixable: true,
33
+ });
34
+ }
35
+ else {
36
+ const type = pkgJson.alpha.component ? 'alpha.component' : 'alpha.module';
37
+ results.push({
38
+ name: `Package "${pkgName}" alpha block`,
39
+ status: 'pass',
40
+ message: `${type} is set`,
41
+ fixable: false,
42
+ });
43
+ }
44
+ }
45
+ return results;
46
+ },
47
+ fix(wsPath) {
48
+ const packages = getPackagesSafe(wsPath);
49
+ if (!packages)
50
+ return;
51
+ for (const pkgPath of packages) {
52
+ const pkgJsonPath = join(pkgPath, 'package.json');
53
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
54
+ const alpha = (pkgJson['alpha'] ?? {});
55
+ if (!alpha['component'] && !alpha['module']) {
56
+ alpha['component'] = true;
57
+ }
58
+ pkgJson['alpha'] = alpha;
59
+ writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 4) + '\n');
60
+ }
61
+ },
62
+ };
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { pluginJsonPath } from '../path-utils.js';
4
+ export const pluginJsonCheck = {
5
+ name: 'plugin.json',
6
+ run(wsPath) {
7
+ const path = pluginJsonPath(wsPath);
8
+ if (!existsSync(path)) {
9
+ return [{ name: 'plugin.json', status: 'fail', message: 'File is missing', fixable: true }];
10
+ }
11
+ let json;
12
+ try {
13
+ json = JSON.parse(readFileSync(path, 'utf-8'));
14
+ }
15
+ catch {
16
+ return [{ name: 'plugin.json', status: 'fail', message: 'File is not valid JSON', fixable: false }];
17
+ }
18
+ const missing = [];
19
+ if (!json.name)
20
+ missing.push('name');
21
+ if (!json.components?.selectorPrefix)
22
+ missing.push('components.selectorPrefix');
23
+ if (!json.modules?.idPrefix)
24
+ missing.push('modules.idPrefix');
25
+ if (missing.length > 0) {
26
+ return [{ name: 'plugin.json', status: 'fail', message: `Missing required fields: ${missing.join(', ')}`, fixable: true }];
27
+ }
28
+ return [{ name: 'plugin.json', status: 'pass', message: 'All required fields present', fixable: false }];
29
+ },
30
+ fix(wsPath) {
31
+ const path = pluginJsonPath(wsPath);
32
+ const defaults = {
33
+ name: basename(wsPath),
34
+ components: { selectorPrefix: 'comp' },
35
+ modules: { idPrefix: 'mod' },
36
+ };
37
+ if (!existsSync(path)) {
38
+ writeFileSync(path, JSON.stringify(defaults, null, 4) + '\n');
39
+ return;
40
+ }
41
+ let existing = {};
42
+ try {
43
+ existing = JSON.parse(readFileSync(path, 'utf-8'));
44
+ }
45
+ catch {
46
+ writeFileSync(path, JSON.stringify(defaults, null, 4) + '\n');
47
+ return;
48
+ }
49
+ existing.name ??= defaults.name;
50
+ existing.components ??= {};
51
+ existing.components.selectorPrefix ??= defaults.components.selectorPrefix;
52
+ existing.modules ??= {};
53
+ existing.modules.idPrefix ??= defaults.modules.idPrefix;
54
+ writeFileSync(path, JSON.stringify(existing, null, 4) + '\n');
55
+ },
56
+ };
@@ -0,0 +1,21 @@
1
+ import { cpSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { pluginServerRoot, pluginServerTemplatesPath } from '../path-utils.js';
4
+ export const pluginsServerCheck = {
5
+ name: 'plugins-server/',
6
+ run(wsPath) {
7
+ const serverDir = pluginServerRoot(wsPath);
8
+ if (!existsSync(serverDir)) {
9
+ return [{ name: 'plugins-server/', status: 'fail', message: 'Directory is missing', fixable: true }];
10
+ }
11
+ if (!existsSync(join(serverDir, 'index.js'))) {
12
+ return [{ name: 'plugins-server/', status: 'fail', message: 'Directory exists but index.js is missing', fixable: true }];
13
+ }
14
+ return [{ name: 'plugins-server/', status: 'pass', message: 'Directory exists with index.js', fixable: false }];
15
+ },
16
+ fix(wsPath) {
17
+ const serverDir = pluginServerRoot(wsPath);
18
+ const templateDir = pluginServerTemplatesPath();
19
+ cpSync(templateDir, serverDir, { recursive: true });
20
+ },
21
+ };
@@ -0,0 +1,51 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export const rootPackageJsonCheck = {
4
+ name: 'Root package.json',
5
+ run(wsPath) {
6
+ const path = join(wsPath, 'package.json');
7
+ if (!existsSync(path)) {
8
+ return [{ name: 'Root package.json', status: 'fail', message: 'File is missing', fixable: false }];
9
+ }
10
+ let json;
11
+ try {
12
+ json = JSON.parse(readFileSync(path, 'utf-8'));
13
+ }
14
+ catch {
15
+ return [{ name: 'Root package.json', status: 'fail', message: 'File is not valid JSON', fixable: false }];
16
+ }
17
+ const issues = [];
18
+ if (!json['workspaces'])
19
+ issues.push('workspaces');
20
+ if (json['type'] !== 'module')
21
+ issues.push('type: "module"');
22
+ const deps = (json['dependencies'] ?? {});
23
+ if (!deps['@jatahworx/alpha-annotations-lib'])
24
+ issues.push('dependency @jatahworx/alpha-annotations-lib');
25
+ const devDeps = (json['devDependencies'] ?? {});
26
+ if (!devDeps['lit'])
27
+ issues.push('devDependency lit');
28
+ if (!devDeps['typescript'])
29
+ issues.push('devDependency typescript');
30
+ if (issues.length > 0) {
31
+ const fixable = issues.includes('workspaces') || issues.includes('type: "module"');
32
+ return [{ name: 'Root package.json', status: 'fail', message: `Missing: ${issues.join(', ')}`, fixable }];
33
+ }
34
+ return [{ name: 'Root package.json', status: 'pass', message: 'workspaces, dependencies, and type configured', fixable: false }];
35
+ },
36
+ fix(wsPath) {
37
+ const path = join(wsPath, 'package.json');
38
+ if (!existsSync(path))
39
+ return;
40
+ let json;
41
+ try {
42
+ json = JSON.parse(readFileSync(path, 'utf-8'));
43
+ }
44
+ catch {
45
+ return;
46
+ }
47
+ json['workspaces'] ??= ['packages/*'];
48
+ json['type'] ??= 'module';
49
+ writeFileSync(path, JSON.stringify(json, null, 4) + '\n');
50
+ },
51
+ };