slicejs-cli 3.5.1 → 3.6.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 (75) hide show
  1. package/README.md +81 -26
  2. package/client.js +73 -23
  3. package/commands/buildProduction/buildProduction.js +6 -3
  4. package/commands/doctor/doctor.js +68 -3
  5. package/commands/getComponent/getComponent.js +33 -25
  6. package/commands/init/init.js +176 -49
  7. package/commands/utils/PackageManager.js +148 -0
  8. package/commands/utils/VersionChecker.js +6 -4
  9. package/commands/utils/sliceScripts.js +23 -0
  10. package/commands/utils/updateManager.js +54 -35
  11. package/package.json +12 -1
  12. package/post.js +13 -19
  13. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
  14. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  15. package/.github/pull_request_template.md +0 -22
  16. package/.github/workflows/ci.yml +0 -43
  17. package/AGENTS.md +0 -247
  18. package/CODE_OF_CONDUCT.md +0 -126
  19. package/ECOSYSTEM.md +0 -9
  20. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +0 -182
  21. package/playwright.config.js +0 -51
  22. package/tests/build-command-integration.test.js +0 -87
  23. package/tests/build-production-e2e.test.js +0 -140
  24. package/tests/builder-edge-cases.test.js +0 -322
  25. package/tests/bundle-generate-e2e.test.js +0 -115
  26. package/tests/bundle-generator.test.js +0 -691
  27. package/tests/bundle-v2-register-output.test.js +0 -470
  28. package/tests/bundling-dependency-edges.test.js +0 -127
  29. package/tests/bundling-imports-unit.test.js +0 -267
  30. package/tests/client-launcher-contract.test.js +0 -211
  31. package/tests/client-update-flow-contract.test.js +0 -272
  32. package/tests/commands-component-crud.test.js +0 -102
  33. package/tests/commands-doctor.test.js +0 -80
  34. package/tests/commands-version-checker.test.js +0 -37
  35. package/tests/component-registry-parse.test.js +0 -34
  36. package/tests/dependency-analyzer.test.js +0 -24
  37. package/tests/e2e/bundles.spec.js +0 -91
  38. package/tests/e2e/dependency-scenarios.spec.js +0 -56
  39. package/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
  40. package/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
  41. package/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
  42. package/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
  43. package/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
  44. package/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
  45. package/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
  46. package/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
  47. package/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
  48. package/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
  49. package/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
  50. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
  51. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
  52. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
  53. package/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
  54. package/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
  55. package/tests/e2e/fixtures/components/registry.json +0 -12
  56. package/tests/e2e/fixtures/vendor-components.mjs +0 -65
  57. package/tests/e2e/navigation.spec.js +0 -44
  58. package/tests/e2e/render.spec.js +0 -34
  59. package/tests/e2e/serve.mjs +0 -264
  60. package/tests/e2e/shared-deps.spec.js +0 -61
  61. package/tests/e2e/unminified.spec.js +0 -33
  62. package/tests/e2e-serve.test.js +0 -148
  63. package/tests/fixtures/components.js +0 -8
  64. package/tests/fixtures/sliceConfig.json +0 -74
  65. package/tests/getcomponent.test.js +0 -407
  66. package/tests/helpers/setup.js +0 -102
  67. package/tests/init-command-contract.test.js +0 -46
  68. package/tests/local-cli-delegation.test.js +0 -81
  69. package/tests/path-helper.test.js +0 -206
  70. package/tests/perf-budget.test.js +0 -86
  71. package/tests/postinstall-command.test.js +0 -72
  72. package/tests/types-breakage.test.js +0 -491
  73. package/tests/types-generator-errors.test.js +0 -361
  74. package/tests/types-generator.test.js +0 -346
  75. package/tests/update-manager-notifications.test.js +0 -88
package/README.md CHANGED
@@ -28,9 +28,9 @@ This repository contains the Slice.js CLI (`slicejs-cli`), the command-line tool
28
28
  cd slicejs-cli
29
29
  ```
30
30
 
31
- 2. **Install dependencies**
31
+ 2. **Install dependencies** (the repo pins pnpm via the `packageManager` field)
32
32
  ```bash
33
- npm install
33
+ pnpm install --frozen-lockfile
34
34
  ```
35
35
 
36
36
  3. **Test changes locally**
@@ -50,20 +50,58 @@ This repository contains the Slice.js CLI (`slicejs-cli`), the command-line tool
50
50
 
51
51
  ## Installation (for users)
52
52
 
53
- ### Local (Recommended)
53
+ For a **new project** you don't install anything manually — `slice init` (see Quick
54
+ start below) installs the CLI locally in the project it creates.
55
+
56
+ ### Local in an existing project (Recommended)
54
57
 
55
58
  ```bash
56
- npm install slicejs-cli --save-dev
59
+ npm install slicejs-cli --save-dev # or: pnpm add -D slicejs-cli
57
60
  ```
58
61
 
59
- ### Global (Not recommended)
62
+ ### Global launcher (optional)
60
63
 
61
64
  ```bash
62
- npm install -g slicejs-cli
65
+ npm install -g slicejs-cli # or: pnpm add -g slicejs-cli
63
66
  ```
64
67
 
68
+ The launcher delegates to the nearest project-local `node_modules/slicejs-cli`,
69
+ so each project keeps its pinned CLI version.
70
+
65
71
  ## Main commands
66
72
 
73
+ Inside initialized projects, prefer package scripts (`pnpm run ...`, `npm run ...`)
74
+ over direct binary calls.
75
+
76
+ Common script workflow:
77
+
78
+ ```bash
79
+ pnpm run dev
80
+ pnpm run build
81
+ pnpm run start
82
+ pnpm run browse
83
+ pnpm run get -- Button
84
+ pnpm run sync
85
+ ```
86
+
87
+ Alternative with local devDependency resolution:
88
+
89
+ ```bash
90
+ pnpm exec slice dev
91
+ ```
92
+
93
+ If `slicejs-cli` is installed globally, `slice` can be executed directly from PATH.
94
+
95
+ For pnpm v10+, if build scripts are restricted, configure `allowBuilds` in
96
+ `pnpm-workspace.yaml`:
97
+
98
+ ```yaml
99
+ allowBuilds:
100
+ slicejs-cli: true
101
+ ```
102
+
103
+ `slice init --pm pnpm` now writes this automatically.
104
+
67
105
  | Command | Description |
68
106
  |---------|-------------|
69
107
  | `slice init` | Initialize a Slice.js project |
@@ -83,17 +121,21 @@ When you install `slicejs-cli`, the `postinstall` script automatically configure
83
121
  ```json
84
122
  {
85
123
  "scripts": {
86
- "slice:dev": "slice dev",
87
- "slice:start": "slice start",
88
- "slice:create": "slice component create",
89
- "slice:list": "slice component list",
90
- "slice:delete": "slice component delete",
91
- "slice:init": "slice init",
92
- "slice:get": "slice get",
93
- "slice:browse": "slice browse",
94
- "slice:sync": "slice sync",
95
- "slice:version": "slice version",
96
- "slice:update": "slice update"
124
+ "slice:init": "node ./node_modules/slicejs-cli/client.js init",
125
+ "slice:dev": "node ./node_modules/slicejs-cli/client.js dev",
126
+ "slice:build": "node ./node_modules/slicejs-cli/client.js build",
127
+ "slice:start": "node ./node_modules/slicejs-cli/client.js start",
128
+ "slice:create": "node ./node_modules/slicejs-cli/client.js component create",
129
+ "slice:list": "node ./node_modules/slicejs-cli/client.js component list",
130
+ "slice:delete": "node ./node_modules/slicejs-cli/client.js component delete",
131
+ "slice:get": "node ./node_modules/slicejs-cli/client.js get",
132
+ "slice:browse": "node ./node_modules/slicejs-cli/client.js browse",
133
+ "slice:sync": "node ./node_modules/slicejs-cli/client.js sync",
134
+ "slice:doctor": "node ./node_modules/slicejs-cli/client.js doctor",
135
+ "slice:version": "node ./node_modules/slicejs-cli/client.js version",
136
+ "slice:help": "node ./node_modules/slicejs-cli/client.js --help",
137
+ "slice:update": "node ./node_modules/slicejs-cli/client.js update",
138
+ "slice:types": "node ./node_modules/slicejs-cli/client.js types generate"
97
139
  }
98
140
  }
99
141
  ```
@@ -106,21 +148,34 @@ npx slicejs-cli postinstall
106
148
 
107
149
  ## Quick start
108
150
 
151
+ `slice init` creates the project folder itself — no `mkdir` or `npm init` needed.
152
+ Everything (package.json, node_modules, lockfile, src/, api/) lives inside the new folder.
153
+
109
154
  ```bash
110
- # 1. Create project
111
- mkdir my-project && cd my-project
112
- npm init -y
155
+ # With npm
156
+ npx slicejs-cli init
157
+ cd my-app
158
+ npm run dev
159
+ ```
113
160
 
114
- # 2. Install CLI
115
- npm install slicejs-cli --save-dev
161
+ ```bash
162
+ # With pnpm
163
+ pnpm dlx slicejs-cli init
164
+ cd my-app
165
+ pnpm run dev
166
+ ```
116
167
 
117
- # 3. Initialize
118
- npx slicejs-cli init
168
+ Non-interactive (for scripts/CI):
119
169
 
120
- # 4. Development
121
- npx slicejs-cli dev
170
+ ```bash
171
+ npx slicejs-cli init -y my-app --pm pnpm
122
172
  ```
123
173
 
174
+ init pins the chosen package manager in the `packageManager` field, installs
175
+ `slicejs-web-framework` as a dependency and `slicejs-cli` as a devDependency of
176
+ the new project. Versions are never hard-pinned at install time, so hardened pnpm
177
+ setups (`minimumReleaseAge` quarantine, `ignore-scripts`) work out of the box.
178
+
124
179
  ## Tests
125
180
 
126
181
  The CLI uses Node.js native test runner:
package/client.js CHANGED
@@ -27,6 +27,15 @@ import {
27
27
  resolveLocalCliCandidate,
28
28
  shouldDelegateToLocalCli
29
29
  } from './commands/utils/LocalCliDelegation.js';
30
+ import {
31
+ detectPackageManager,
32
+ getAvailablePackageManagers,
33
+ isPackageManagerAvailable,
34
+ resolvePackageManager,
35
+ runScriptCommand,
36
+ SUPPORTED_PACKAGE_MANAGERS
37
+ } from './commands/utils/PackageManager.js';
38
+ import { SLICE_SCRIPTS } from './commands/utils/sliceScripts.js';
30
39
 
31
40
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
41
 
@@ -96,6 +105,7 @@ sliceClient
96
105
  .command("init")
97
106
  .description("Initialize a new Slice.js project")
98
107
  .option("-y, --yes [name]", "Skip prompts and initialize with project name")
108
+ .option("--pm <packageManager>", "Package manager to use (pnpm or npm). Auto-detected when omitted")
99
109
  .action(async (options) => {
100
110
  let projectName = 'my-slice-app';
101
111
  if (options.yes) {
@@ -123,16 +133,62 @@ sliceClient
123
133
  projectName = answers.projectName;
124
134
  }
125
135
 
136
+ // Resolve the package manager: --pm flag → detection → interactive prompt.
137
+ // Detection here has no project root yet (the folder is new), so it relies on
138
+ // npm_config_user_agent (npx / pnpm dlx / PM scripts) or a single available binary.
139
+ let packageManager = options.pm;
140
+ if (packageManager && !SUPPORTED_PACKAGE_MANAGERS.includes(packageManager)) {
141
+ Print.error(`Unsupported package manager "${packageManager}". Use one of: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
142
+ return;
143
+ }
144
+ if (packageManager && !isPackageManagerAvailable(packageManager)) {
145
+ Print.error(`Package manager "${packageManager}" is not available on this system`);
146
+ return;
147
+ }
148
+ if (!packageManager) {
149
+ const detected = detectPackageManager(null);
150
+ if (detected) {
151
+ packageManager = detected.name;
152
+ } else if (!options.yes) {
153
+ const available = getAvailablePackageManagers();
154
+ if (available.length === 0) {
155
+ Print.error('No package manager found (pnpm or npm). Install one and retry.');
156
+ return;
157
+ }
158
+ const { pm } = await inquirer.prompt([
159
+ {
160
+ type: 'list',
161
+ name: 'pm',
162
+ message: 'Which package manager do you want to use?',
163
+ choices: available,
164
+ default: available.includes('pnpm') ? 'pnpm' : available[0]
165
+ }
166
+ ]);
167
+ packageManager = pm;
168
+ } else {
169
+ const available = getAvailablePackageManagers();
170
+ packageManager = available.includes('pnpm') ? 'pnpm' : available[0];
171
+ if (!packageManager) {
172
+ Print.error('No package manager found (pnpm or npm). Install one and retry.');
173
+ return;
174
+ }
175
+ }
176
+ }
177
+
126
178
  const projectDir = path.resolve(projectName);
127
179
 
128
180
  if (fs.existsSync(projectDir)) {
129
181
  const contents = fs.readdirSync(projectDir);
130
182
  if (contents.length > 0) {
183
+ if (options.yes) {
184
+ Print.error(`Directory "${projectName}" already exists and is not empty. Aborting.`);
185
+ return;
186
+ }
131
187
  const { overwrite } = await inquirer.prompt([
132
188
  {
133
189
  type: 'confirm',
134
190
  name: 'overwrite',
135
- message: `Directory "${answers.projectName}" already exists and is not empty. Continue?`,
191
+ message: `Directory "${projectName}" already exists and is not empty. Continue?`,
136
192
  default: false
137
193
  }
138
194
  ]);
@@ -148,9 +204,8 @@ sliceClient
148
204
  process.chdir(projectDir);
149
205
  process.env.INIT_CWD = projectDir;
150
206
 
151
- await runWithVersionCheck(() => {
152
- initializeProject();
153
- return Promise.resolve();
207
+ await runWithVersionCheck(async () => {
208
+ await initializeProject({ packageManager });
154
209
  });
155
210
  });
156
211
 
@@ -353,7 +408,7 @@ componentCommand
353
408
  category = categoryAnswer.category;
354
409
  }
355
410
 
356
- const config = loadConfig();
411
+ const config = sharedLoadConfigSync(import.meta.url);
357
412
  if (!config) {
358
413
  Print.error("Could not load configuration");
359
414
  return;
@@ -550,6 +605,7 @@ sliceClient
550
605
  .option("-y, --yes", "Skip confirmation and update all packages automatically")
551
606
  .option("--cli", "Update only the Slice.js CLI")
552
607
  .option("-f, --framework", "Update only the Slice.js Framework")
608
+ .option("--update-api", "Also overwrite api/index.js with the framework version (never done by default; creates a .bak backup)")
553
609
  .action(async (options) => {
554
610
  await updateManager.checkAndPromptUpdates(options);
555
611
  });
@@ -570,30 +626,24 @@ sliceClient
570
626
  .command("postinstall")
571
627
  .description("Configure npm scripts in package.json (alternative to postinstall for --ignore-scripts users)")
572
628
  .action(() => {
573
- const isGlobal = process.env.npm_config_global === 'true';
629
+ // npm sets npm_config_global; pnpm global installs are detected via PNPM_HOME.
630
+ const pnpmHome = process.env.PNPM_HOME;
631
+ const cliEntryPath = fileURLToPath(import.meta.url);
632
+ const isGlobal = process.env.npm_config_global === 'true'
633
+ || (pnpmHome && cliEntryPath.startsWith(pnpmHome));
574
634
  if (isGlobal) {
575
635
  console.log('⚠️ Global installation of slicejs-cli detected.');
576
636
  console.log(' We strongly recommend using a local installation to avoid version mismatches.');
577
- console.log(' Uninstall global: npm uninstall -g slicejs-cli');
637
+ console.log(` Uninstall global: ${pnpmHome ? 'pnpm remove -g slicejs-cli' : 'npm uninstall -g slicejs-cli'}`);
578
638
  return;
579
639
  }
580
640
 
581
641
  const projectRoot = getProjectRoot(import.meta.url);
582
642
  const pkgPath = path.join(projectRoot, 'package.json');
643
+ const packageManager = resolvePackageManager(projectRoot).name;
583
644
 
584
- const sliceScripts = {
585
- 'slice:dev': 'slice dev',
586
- 'slice:start': 'slice start',
587
- 'slice:create': 'slice component create',
588
- 'slice:list': 'slice component list',
589
- 'slice:delete': 'slice component delete',
590
- 'slice:init': 'slice init',
591
- 'slice:get': 'slice get',
592
- 'slice:browse': 'slice browse',
593
- 'slice:sync': 'slice sync',
594
- 'slice:version': 'slice version',
595
- 'slice:update': 'slice update',
596
- };
645
+ // Shared with post.js and slice init — see commands/utils/sliceScripts.js
646
+ const sliceScripts = SLICE_SCRIPTS;
597
647
 
598
648
  try {
599
649
  let pkg = {};
@@ -618,12 +668,12 @@ sliceClient
618
668
  }
619
669
 
620
670
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8');
621
- console.log(`✅ slicejs-cli installed successfully. Added ${addedCount} npm scripts to package.json.`);
622
- console.log(' Run: npm run slice:dev');
671
+ console.log(`✅ slicejs-cli installed successfully. Added ${addedCount} package scripts to package.json.`);
672
+ console.log(` Run: ${runScriptCommand(packageManager, 'slice:dev')}`);
623
673
  } catch (err) {
624
674
  console.log('✅ slicejs-cli installed successfully.');
625
675
  console.log(' Could not auto-configure scripts:', err.message);
626
- console.log(' Run: npx slice dev');
676
+ console.log(` Configure scripts manually and run: ${runScriptCommand(packageManager, 'slice:dev')}`);
627
677
  }
628
678
  });
629
679
 
@@ -6,7 +6,8 @@ import { minify as terserMinify } from 'terser';
6
6
  import { minify } from 'html-minifier-terser';
7
7
  import CleanCSS from 'clean-css';
8
8
  import Print from '../Print.js';
9
- import { getSrcPath, getDistPath, getConfigPath } from '../utils/PathHelper.js';
9
+ import { getSrcPath, getDistPath, getConfigPath, getProjectRoot } from '../utils/PathHelper.js';
10
+ import { resolvePackageManager, runScriptCommand } from '../utils/PackageManager.js';
10
11
 
11
12
  /**
12
13
  * Loads configuration from sliceConfig.json
@@ -441,6 +442,7 @@ async function analyzeBuild() {
441
442
  */
442
443
  export default async function buildProduction(options = {}) {
443
444
  const startTime = Date.now();
445
+ const packageManager = resolvePackageManager(getProjectRoot(import.meta.url)).name;
444
446
 
445
447
  try {
446
448
  Print.title('🔨 Building Slice.js project for production...');
@@ -483,7 +485,7 @@ export default async function buildProduction(options = {}) {
483
485
  Print.info('Your optimized project is ready in the /dist directory');
484
486
  Print.newLine();
485
487
  Print.info('Next steps:');
486
- console.log(' • Use "npm run slice:start" to test the production build');
488
+ console.log(` • Use "${runScriptCommand(packageManager, 'start')}" to test the production build`);
487
489
  console.log(' • All Slice.js components and architecture preserved');
488
490
 
489
491
  return true;
@@ -499,6 +501,7 @@ export default async function buildProduction(options = {}) {
499
501
  */
500
502
  export async function serveProductionBuild(port) {
501
503
  try {
504
+ const packageManager = resolvePackageManager(getProjectRoot(import.meta.url)).name;
502
505
  const config = loadConfig();
503
506
  const defaultPort = config?.server?.port || 3001;
504
507
  const finalPort = port || defaultPort;
@@ -533,7 +536,7 @@ export async function serveProductionBuild(port) {
533
536
  Print.success(`Production preview server running at http://localhost:${finalPort}`);
534
537
  Print.info('Press Ctrl+C to stop the server');
535
538
  Print.info('This server previews your production build from /dist');
536
- Print.warning('This is a preview server - use "npm run slice:start" for the full production server');
539
+ Print.warning(`This is a preview server - use "${runScriptCommand(packageManager, 'start')}" for the full production server`);
537
540
  });
538
541
 
539
542
  } catch (error) {
@@ -8,6 +8,7 @@ import inquirer from 'inquirer';
8
8
  import { exec } from 'child_process';
9
9
  import { promisify } from 'util';
10
10
  import { getProjectRoot, getSrcPath, getApiPath, getConfigPath, getPath } from '../utils/PathHelper.js';
11
+ import { resolvePackageManager, installCommand } from '../utils/PackageManager.js';
11
12
  import updateManager from '../utils/updateManager.js';
12
13
 
13
14
  /**
@@ -142,6 +143,68 @@ async function checkPort() {
142
143
  });
143
144
  }
144
145
 
146
+ /**
147
+ * Checks the package manager setup: mixed lockfiles, packageManager field
148
+ * consistency, and a project-local CLI install (required for local delegation).
149
+ * Exported for tests.
150
+ */
151
+ export async function checkPackageManagerSetup(projectRoot = getProjectRoot(import.meta.url)) {
152
+ const issues = [];
153
+ const suggestions = [];
154
+
155
+ const LOCKFILE_PM = {
156
+ 'package-lock.json': 'npm',
157
+ 'pnpm-lock.yaml': 'pnpm'
158
+ };
159
+ const lockfiles = [];
160
+ for (const lockfile of Object.keys(LOCKFILE_PM)) {
161
+ if (await fs.pathExists(path.join(projectRoot, lockfile))) {
162
+ lockfiles.push(lockfile);
163
+ }
164
+ }
165
+
166
+ let pkg = null;
167
+ const pkgPath = path.join(projectRoot, 'package.json');
168
+ if (await fs.pathExists(pkgPath)) {
169
+ try { pkg = await fs.readJson(pkgPath); } catch { /* reported by Dependencies check */ }
170
+ }
171
+ const pmField = typeof pkg?.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
172
+ const pm = resolvePackageManager(projectRoot).name;
173
+
174
+ if (lockfiles.length > 1) {
175
+ issues.push(`Mixed lockfiles found: ${lockfiles.join(', ')}`);
176
+ const keep = pmField || pm;
177
+ const keepLockfile = Object.entries(LOCKFILE_PM).find(([, name]) => name === keep)?.[0];
178
+ suggestions.push(`Keep only ${keepLockfile ?? 'the lockfile of your package manager'} and delete the rest`);
179
+ }
180
+
181
+ if (pkg && !pmField) {
182
+ issues.push('No "packageManager" field in package.json');
183
+ suggestions.push(`Pin it (e.g. "packageManager": "${pm}@<version>") so every tool resolves the same package manager`);
184
+ } else if (pmField && lockfiles.length === 1 && LOCKFILE_PM[lockfiles[0]] !== pmField) {
185
+ issues.push(`packageManager is "${pmField}" but the lockfile is ${lockfiles[0]}`);
186
+ suggestions.push(`Reinstall with ${pmField} (or update the packageManager field) so they agree`);
187
+ }
188
+
189
+ const localCliPath = path.join(projectRoot, 'node_modules', 'slicejs-cli', 'package.json');
190
+ if (pkg && !(await fs.pathExists(localCliPath))) {
191
+ issues.push('slicejs-cli is not installed locally');
192
+ suggestions.push(`Run "${installCommand(pm, 'slicejs-cli', { dev: true })}" so the launcher delegates to a version pinned per project`);
193
+ }
194
+
195
+ if (issues.length === 0) {
196
+ return {
197
+ pass: true,
198
+ message: `Package manager setup is consistent (${pmField || pm})`
199
+ };
200
+ }
201
+ return {
202
+ warn: true,
203
+ message: issues.join(' | '),
204
+ suggestion: suggestions.join(' | ')
205
+ };
206
+ }
207
+
145
208
  /**
146
209
  * Checks dependencies in package.json
147
210
  */
@@ -176,8 +239,8 @@ async function checkDependencies() {
176
239
  warn: true,
177
240
  message: `Missing dependencies: ${missing.join(', ')}`,
178
241
  suggestion: missing.includes('slicejs-web-framework')
179
- ? 'Run "npm install slicejs-web-framework@latest" in your project'
180
- : 'Run "npm install -D slicejs-cli@latest" in your project',
242
+ ? `Run "${installCommand(resolvePackageManager(getProjectRoot(import.meta.url)).name, 'slicejs-web-framework@latest')}" in your project`
243
+ : `Run "${installCommand(resolvePackageManager(getProjectRoot(import.meta.url)).name, 'slicejs-cli@latest', { dev: true })}" in your project`,
181
244
  missing
182
245
  };
183
246
  }
@@ -278,6 +341,7 @@ export default async function runDiagnostics() {
278
341
  { name: 'Project Structure', fn: checkDirectoryStructure },
279
342
  { name: 'Configuration', fn: checkConfig },
280
343
  { name: 'Port Availability', fn: checkPort },
344
+ { name: 'Package Manager', fn: checkPackageManagerSetup },
281
345
  { name: 'Dependencies', fn: checkDependencies },
282
346
  { name: 'Components', fn: checkComponents }
283
347
  ];
@@ -340,9 +404,10 @@ export default async function runDiagnostics() {
340
404
  }
341
405
  ]);
342
406
  if (confirmInstall) {
407
+ const pm = resolvePackageManager(projectRoot).name;
343
408
  for (const pkg of depsResult.missing) {
344
409
  try {
345
- const cmd = 'npm install slicejs-web-framework@latest';
410
+ const cmd = installCommand(pm, `${pkg}@latest`);
346
411
  Print.info(`Installing ${pkg}...`);
347
412
  await execAsync(cmd, { cwd: projectRoot });
348
413
  Print.success(`${pkg} installed`);
@@ -50,6 +50,10 @@ class ComponentRegistry {
50
50
  this.componentsRegistry = null;
51
51
  this.config = null;
52
52
  this._configPromise = null;
53
+ // Serializes read-modify-write cycles on components.js: installs run
54
+ // concurrently (runConcurrent), and parallel writers corrupt the file
55
+ // (partial reads) or silently drop registrations (lost updates).
56
+ this._registryLock = Promise.resolve();
53
57
  }
54
58
 
55
59
  async _ensureConfig() {
@@ -249,31 +253,35 @@ filterOfficialComponents(allComponents) {
249
253
  }
250
254
 
251
255
  async updateLocalRegistrySafe(componentName, category) {
256
+ // Queue behind any in-flight registry update; keep the chain alive even
257
+ // when an update throws so later updates still run.
258
+ const run = this._registryLock.then(() => this._updateLocalRegistry(componentName, category));
259
+ this._registryLock = run.catch(() => {});
260
+ return run;
261
+ }
262
+
263
+ async _updateLocalRegistry(componentName, category) {
252
264
  const componentsPath = getComponentsJsPath(import.meta.url);
253
- try {
254
- if (!await fs.pathExists(componentsPath)) {
255
- const dir = path.dirname(componentsPath);
256
- await fs.ensureDir(dir);
257
- const initial = `const components = {};\n\nexport default components;\n`;
258
- await fs.writeFile(componentsPath, initial, 'utf8');
259
- }
260
- const content = await fs.readFile(componentsPath, 'utf8');
261
- const match = content.match(/const components = ({[\s\S]*?});/);
262
- if (!match) throw new Error('Invalid components.js format in local project');
263
- const componentsObj = JSON.parse(match[1]);
264
- if (!componentsObj[componentName]) {
265
- componentsObj[componentName] = category;
266
- const sorted = Object.keys(componentsObj)
267
- .sort()
268
- .reduce((obj, key) => { obj[key] = componentsObj[key]; return obj; }, {});
269
- const newContent = `const components = ${JSON.stringify(sorted, null, 2)};\n\nexport default components;\n`;
270
- await fs.writeFile(componentsPath, newContent, 'utf8');
271
- Print.registryUpdate(`Registered ${componentName} in local components.js`);
272
- } else {
273
- Print.info(`${componentName} already exists in local registry`);
274
- }
275
- } catch (error) {
276
- throw error;
265
+ if (!await fs.pathExists(componentsPath)) {
266
+ const dir = path.dirname(componentsPath);
267
+ await fs.ensureDir(dir);
268
+ const initial = `const components = {};\n\nexport default components;\n`;
269
+ await fs.writeFile(componentsPath, initial, 'utf8');
270
+ }
271
+ const content = await fs.readFile(componentsPath, 'utf8');
272
+ const match = content.match(/const components = ({[\s\S]*?});/);
273
+ if (!match) throw new Error('Invalid components.js format in local project');
274
+ const componentsObj = JSON.parse(match[1]);
275
+ if (!componentsObj[componentName]) {
276
+ componentsObj[componentName] = category;
277
+ const sorted = Object.keys(componentsObj)
278
+ .sort()
279
+ .reduce((obj, key) => { obj[key] = componentsObj[key]; return obj; }, {});
280
+ const newContent = `const components = ${JSON.stringify(sorted, null, 2)};\n\nexport default components;\n`;
281
+ await fs.writeFile(componentsPath, newContent, 'utf8');
282
+ Print.registryUpdate(`Registered ${componentName} in local components.js`);
283
+ } else {
284
+ Print.info(`${componentName} already exists in local registry`);
277
285
  }
278
286
  }
279
287
 
@@ -339,7 +347,7 @@ filterOfficialComponents(allComponents) {
339
347
  await this.updateLocalRegistrySafe(componentName, category);
340
348
 
341
349
  Print.success(`${componentName} installed successfully from official repository!`);
342
- console.log(`📁 Location: ${folderSuffix}/${categoryPath}/${componentName}/`);
350
+ console.log(`📁 Location: ${[folderSuffix, categoryPath, componentName].join('/').replace(/\/+/g, '/')}/`);
343
351
  console.log(`📄 Files: ${downloadedFiles.join(', ')}`);
344
352
 
345
353
  return true;