slicejs-cli 3.5.1 → 3.6.0

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 (74) hide show
  1. package/README.md +34 -15
  2. package/client.js +67 -20
  3. package/commands/doctor/doctor.js +69 -3
  4. package/commands/getComponent/getComponent.js +33 -25
  5. package/commands/init/init.js +106 -28
  6. package/commands/utils/PackageManager.js +148 -0
  7. package/commands/utils/VersionChecker.js +6 -4
  8. package/commands/utils/sliceScripts.js +21 -0
  9. package/commands/utils/updateManager.js +54 -35
  10. package/package.json +12 -1
  11. package/post.js +8 -16
  12. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
  13. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  14. package/.github/pull_request_template.md +0 -22
  15. package/.github/workflows/ci.yml +0 -43
  16. package/AGENTS.md +0 -247
  17. package/CODE_OF_CONDUCT.md +0 -126
  18. package/ECOSYSTEM.md +0 -9
  19. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +0 -182
  20. package/playwright.config.js +0 -51
  21. package/tests/build-command-integration.test.js +0 -87
  22. package/tests/build-production-e2e.test.js +0 -140
  23. package/tests/builder-edge-cases.test.js +0 -322
  24. package/tests/bundle-generate-e2e.test.js +0 -115
  25. package/tests/bundle-generator.test.js +0 -691
  26. package/tests/bundle-v2-register-output.test.js +0 -470
  27. package/tests/bundling-dependency-edges.test.js +0 -127
  28. package/tests/bundling-imports-unit.test.js +0 -267
  29. package/tests/client-launcher-contract.test.js +0 -211
  30. package/tests/client-update-flow-contract.test.js +0 -272
  31. package/tests/commands-component-crud.test.js +0 -102
  32. package/tests/commands-doctor.test.js +0 -80
  33. package/tests/commands-version-checker.test.js +0 -37
  34. package/tests/component-registry-parse.test.js +0 -34
  35. package/tests/dependency-analyzer.test.js +0 -24
  36. package/tests/e2e/bundles.spec.js +0 -91
  37. package/tests/e2e/dependency-scenarios.spec.js +0 -56
  38. package/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
  39. package/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
  40. package/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
  41. package/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
  42. package/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
  43. package/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
  44. package/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
  45. package/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
  46. package/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
  47. package/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
  48. package/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
  49. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
  50. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
  51. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
  52. package/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
  53. package/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
  54. package/tests/e2e/fixtures/components/registry.json +0 -12
  55. package/tests/e2e/fixtures/vendor-components.mjs +0 -65
  56. package/tests/e2e/navigation.spec.js +0 -44
  57. package/tests/e2e/render.spec.js +0 -34
  58. package/tests/e2e/serve.mjs +0 -264
  59. package/tests/e2e/shared-deps.spec.js +0 -61
  60. package/tests/e2e/unminified.spec.js +0 -33
  61. package/tests/e2e-serve.test.js +0 -148
  62. package/tests/fixtures/components.js +0 -8
  63. package/tests/fixtures/sliceConfig.json +0 -74
  64. package/tests/getcomponent.test.js +0 -407
  65. package/tests/helpers/setup.js +0 -102
  66. package/tests/init-command-contract.test.js +0 -46
  67. package/tests/local-cli-delegation.test.js +0 -81
  68. package/tests/path-helper.test.js +0 -206
  69. package/tests/perf-budget.test.js +0 -86
  70. package/tests/postinstall-command.test.js +0 -72
  71. package/tests/types-breakage.test.js +0 -491
  72. package/tests/types-generator-errors.test.js +0 -361
  73. package/tests/types-generator.test.js +0 -346
  74. 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,18 +50,24 @@ 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
 
67
73
  | Command | Description |
@@ -106,21 +112,34 @@ npx slicejs-cli postinstall
106
112
 
107
113
  ## Quick start
108
114
 
115
+ `slice init` creates the project folder itself — no `mkdir` or `npm init` needed.
116
+ Everything (package.json, node_modules, lockfile, src/, api/) lives inside the new folder.
117
+
109
118
  ```bash
110
- # 1. Create project
111
- mkdir my-project && cd my-project
112
- npm init -y
119
+ # With npm
120
+ npx slicejs-cli init
121
+ cd my-app
122
+ npm run dev
123
+ ```
113
124
 
114
- # 2. Install CLI
115
- npm install slicejs-cli --save-dev
125
+ ```bash
126
+ # With pnpm
127
+ pnpm dlx slicejs-cli init
128
+ cd my-app
129
+ pnpm run dev
130
+ ```
116
131
 
117
- # 3. Initialize
118
- npx slicejs-cli init
132
+ Non-interactive (for scripts/CI):
119
133
 
120
- # 4. Development
121
- npx slicejs-cli dev
134
+ ```bash
135
+ npx slicejs-cli init -y my-app --pm pnpm
122
136
  ```
123
137
 
138
+ init pins the chosen package manager in the `packageManager` field, installs
139
+ `slicejs-web-framework` as a dependency and `slicejs-cli` as a devDependency of
140
+ the new project. Versions are never hard-pinned at install time, so hardened pnpm
141
+ setups (`minimumReleaseAge` quarantine, `ignore-scripts`) work out of the box.
142
+
124
143
  ## Tests
125
144
 
126
145
  The CLI uses Node.js native test runner:
package/client.js CHANGED
@@ -27,6 +27,13 @@ import {
27
27
  resolveLocalCliCandidate,
28
28
  shouldDelegateToLocalCli
29
29
  } from './commands/utils/LocalCliDelegation.js';
30
+ import {
31
+ detectPackageManager,
32
+ getAvailablePackageManagers,
33
+ isPackageManagerAvailable,
34
+ SUPPORTED_PACKAGE_MANAGERS
35
+ } from './commands/utils/PackageManager.js';
36
+ import { SLICE_SCRIPTS } from './commands/utils/sliceScripts.js';
30
37
 
31
38
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
39
 
@@ -96,6 +103,7 @@ sliceClient
96
103
  .command("init")
97
104
  .description("Initialize a new Slice.js project")
98
105
  .option("-y, --yes [name]", "Skip prompts and initialize with project name")
106
+ .option("--pm <packageManager>", "Package manager to use (npm, pnpm or yarn). Auto-detected when omitted")
99
107
  .action(async (options) => {
100
108
  let projectName = 'my-slice-app';
101
109
  if (options.yes) {
@@ -123,16 +131,62 @@ sliceClient
123
131
  projectName = answers.projectName;
124
132
  }
125
133
 
134
+ // Resolve the package manager: --pm flag → detection → interactive prompt.
135
+ // Detection here has no project root yet (the folder is new), so it relies on
136
+ // npm_config_user_agent (npx / pnpm dlx / PM scripts) or a single available binary.
137
+ let packageManager = options.pm;
138
+ if (packageManager && !SUPPORTED_PACKAGE_MANAGERS.includes(packageManager)) {
139
+ Print.error(`Unsupported package manager "${packageManager}". Use one of: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
140
+ return;
141
+ }
142
+ if (packageManager && !isPackageManagerAvailable(packageManager)) {
143
+ Print.error(`Package manager "${packageManager}" is not available on this system`);
144
+ return;
145
+ }
146
+ if (!packageManager) {
147
+ const detected = detectPackageManager(null);
148
+ if (detected) {
149
+ packageManager = detected.name;
150
+ } else if (!options.yes) {
151
+ const available = getAvailablePackageManagers();
152
+ if (available.length === 0) {
153
+ Print.error('No package manager found (npm, pnpm or yarn). Install one and retry.');
154
+ return;
155
+ }
156
+ const { pm } = await inquirer.prompt([
157
+ {
158
+ type: 'list',
159
+ name: 'pm',
160
+ message: 'Which package manager do you want to use?',
161
+ choices: available,
162
+ default: available.includes('pnpm') ? 'pnpm' : available[0]
163
+ }
164
+ ]);
165
+ packageManager = pm;
166
+ } else {
167
+ const available = getAvailablePackageManagers();
168
+ packageManager = available.includes('npm') ? 'npm' : available[0];
169
+ if (!packageManager) {
170
+ Print.error('No package manager found (npm, pnpm or yarn). Install one and retry.');
171
+ return;
172
+ }
173
+ }
174
+ }
175
+
126
176
  const projectDir = path.resolve(projectName);
127
177
 
128
178
  if (fs.existsSync(projectDir)) {
129
179
  const contents = fs.readdirSync(projectDir);
130
180
  if (contents.length > 0) {
181
+ if (options.yes) {
182
+ Print.error(`Directory "${projectName}" already exists and is not empty. Aborting.`);
183
+ return;
184
+ }
131
185
  const { overwrite } = await inquirer.prompt([
132
186
  {
133
187
  type: 'confirm',
134
188
  name: 'overwrite',
135
- message: `Directory "${answers.projectName}" already exists and is not empty. Continue?`,
189
+ message: `Directory "${projectName}" already exists and is not empty. Continue?`,
136
190
  default: false
137
191
  }
138
192
  ]);
@@ -148,9 +202,8 @@ sliceClient
148
202
  process.chdir(projectDir);
149
203
  process.env.INIT_CWD = projectDir;
150
204
 
151
- await runWithVersionCheck(() => {
152
- initializeProject();
153
- return Promise.resolve();
205
+ await runWithVersionCheck(async () => {
206
+ await initializeProject({ packageManager });
154
207
  });
155
208
  });
156
209
 
@@ -353,7 +406,7 @@ componentCommand
353
406
  category = categoryAnswer.category;
354
407
  }
355
408
 
356
- const config = loadConfig();
409
+ const config = sharedLoadConfigSync(import.meta.url);
357
410
  if (!config) {
358
411
  Print.error("Could not load configuration");
359
412
  return;
@@ -550,6 +603,7 @@ sliceClient
550
603
  .option("-y, --yes", "Skip confirmation and update all packages automatically")
551
604
  .option("--cli", "Update only the Slice.js CLI")
552
605
  .option("-f, --framework", "Update only the Slice.js Framework")
606
+ .option("--update-api", "Also overwrite api/index.js with the framework version (never done by default; creates a .bak backup)")
553
607
  .action(async (options) => {
554
608
  await updateManager.checkAndPromptUpdates(options);
555
609
  });
@@ -570,30 +624,23 @@ sliceClient
570
624
  .command("postinstall")
571
625
  .description("Configure npm scripts in package.json (alternative to postinstall for --ignore-scripts users)")
572
626
  .action(() => {
573
- const isGlobal = process.env.npm_config_global === 'true';
627
+ // npm sets npm_config_global; pnpm global installs are detected via PNPM_HOME.
628
+ const pnpmHome = process.env.PNPM_HOME;
629
+ const cliEntryPath = fileURLToPath(import.meta.url);
630
+ const isGlobal = process.env.npm_config_global === 'true'
631
+ || (pnpmHome && cliEntryPath.startsWith(pnpmHome));
574
632
  if (isGlobal) {
575
633
  console.log('⚠️ Global installation of slicejs-cli detected.');
576
634
  console.log(' We strongly recommend using a local installation to avoid version mismatches.');
577
- console.log(' Uninstall global: npm uninstall -g slicejs-cli');
635
+ console.log(` Uninstall global: ${pnpmHome ? 'pnpm remove -g slicejs-cli' : 'npm uninstall -g slicejs-cli'}`);
578
636
  return;
579
637
  }
580
638
 
581
639
  const projectRoot = getProjectRoot(import.meta.url);
582
640
  const pkgPath = path.join(projectRoot, 'package.json');
583
641
 
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
- };
642
+ // Shared with post.js and slice init — see commands/utils/sliceScripts.js
643
+ const sliceScripts = SLICE_SCRIPTS;
597
644
 
598
645
  try {
599
646
  let pkg = {};
@@ -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,69 @@ 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
+ 'yarn.lock': 'yarn'
159
+ };
160
+ const lockfiles = [];
161
+ for (const lockfile of Object.keys(LOCKFILE_PM)) {
162
+ if (await fs.pathExists(path.join(projectRoot, lockfile))) {
163
+ lockfiles.push(lockfile);
164
+ }
165
+ }
166
+
167
+ let pkg = null;
168
+ const pkgPath = path.join(projectRoot, 'package.json');
169
+ if (await fs.pathExists(pkgPath)) {
170
+ try { pkg = await fs.readJson(pkgPath); } catch { /* reported by Dependencies check */ }
171
+ }
172
+ const pmField = typeof pkg?.packageManager === 'string' ? pkg.packageManager.split('@')[0] : null;
173
+ const pm = resolvePackageManager(projectRoot).name;
174
+
175
+ if (lockfiles.length > 1) {
176
+ issues.push(`Mixed lockfiles found: ${lockfiles.join(', ')}`);
177
+ const keep = pmField || pm;
178
+ const keepLockfile = Object.entries(LOCKFILE_PM).find(([, name]) => name === keep)?.[0];
179
+ suggestions.push(`Keep only ${keepLockfile ?? 'the lockfile of your package manager'} and delete the rest`);
180
+ }
181
+
182
+ if (pkg && !pmField) {
183
+ issues.push('No "packageManager" field in package.json');
184
+ suggestions.push(`Pin it (e.g. "packageManager": "${pm}@<version>") so every tool resolves the same package manager`);
185
+ } else if (pmField && lockfiles.length === 1 && LOCKFILE_PM[lockfiles[0]] !== pmField) {
186
+ issues.push(`packageManager is "${pmField}" but the lockfile is ${lockfiles[0]}`);
187
+ suggestions.push(`Reinstall with ${pmField} (or update the packageManager field) so they agree`);
188
+ }
189
+
190
+ const localCliPath = path.join(projectRoot, 'node_modules', 'slicejs-cli', 'package.json');
191
+ if (pkg && !(await fs.pathExists(localCliPath))) {
192
+ issues.push('slicejs-cli is not installed locally');
193
+ suggestions.push(`Run "${installCommand(pm, 'slicejs-cli', { dev: true })}" so the launcher delegates to a version pinned per project`);
194
+ }
195
+
196
+ if (issues.length === 0) {
197
+ return {
198
+ pass: true,
199
+ message: `Package manager setup is consistent (${pmField || pm})`
200
+ };
201
+ }
202
+ return {
203
+ warn: true,
204
+ message: issues.join(' | '),
205
+ suggestion: suggestions.join(' | ')
206
+ };
207
+ }
208
+
145
209
  /**
146
210
  * Checks dependencies in package.json
147
211
  */
@@ -176,8 +240,8 @@ async function checkDependencies() {
176
240
  warn: true,
177
241
  message: `Missing dependencies: ${missing.join(', ')}`,
178
242
  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',
243
+ ? `Run "${installCommand(resolvePackageManager(getProjectRoot(import.meta.url)).name, 'slicejs-web-framework@latest')}" in your project`
244
+ : `Run "${installCommand(resolvePackageManager(getProjectRoot(import.meta.url)).name, 'slicejs-cli@latest', { dev: true })}" in your project`,
181
245
  missing
182
246
  };
183
247
  }
@@ -278,6 +342,7 @@ export default async function runDiagnostics() {
278
342
  { name: 'Project Structure', fn: checkDirectoryStructure },
279
343
  { name: 'Configuration', fn: checkConfig },
280
344
  { name: 'Port Availability', fn: checkPort },
345
+ { name: 'Package Manager', fn: checkPackageManagerSetup },
281
346
  { name: 'Dependencies', fn: checkDependencies },
282
347
  { name: 'Components', fn: checkComponents }
283
348
  ];
@@ -340,9 +405,10 @@ export default async function runDiagnostics() {
340
405
  }
341
406
  ]);
342
407
  if (confirmInstall) {
408
+ const pm = resolvePackageManager(projectRoot).name;
343
409
  for (const pkg of depsResult.missing) {
344
410
  try {
345
- const cmd = 'npm install slicejs-web-framework@latest';
411
+ const cmd = installCommand(pm, `${pkg}@latest`);
346
412
  Print.info(`Installing ${pkg}...`);
347
413
  await execAsync(cmd, { cwd: projectRoot });
348
414
  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;
@@ -5,10 +5,62 @@ import ora from 'ora';
5
5
  import Print from '../Print.js';
6
6
  import { getProjectRoot, getApiPath, getSrcPath, getPath } from '../utils/PathHelper.js';
7
7
  import { execSync } from 'child_process';
8
+ import {
9
+ resolvePackageManager,
10
+ getPackageManagerVersion,
11
+ installCommand
12
+ } from '../utils/PackageManager.js';
13
+ import { SLICE_SCRIPTS } from '../utils/sliceScripts.js';
8
14
 
9
15
  // Import ComponentRegistry class from getComponent
10
16
  import { ComponentRegistry } from '../getComponent/getComponent.js';
11
17
 
18
+ // Fetch the latest published version straight from the npm registry. This is
19
+ // informational only (we never pin installs to it): it avoids depending on
20
+ // `npm view` (absent on pnpm-only machines) and plays nice with pnpm's
21
+ // minimumReleaseAge quarantine, which may legitimately resolve an older version.
22
+ async function fetchLatestVersion(packageName) {
23
+ try {
24
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
25
+ headers: { 'Accept': 'application/json' }
26
+ });
27
+ if (!response.ok) return null;
28
+ const data = await response.json();
29
+ return data.version || null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ // Create the project manifest BEFORE any install runs. Without a package.json in
36
+ // the project folder, npm/pnpm walk up the directory tree looking for the nearest
37
+ // manifest and anchor node_modules (and the dependency entry) OUTSIDE the project.
38
+ // Exported for tests (init-project-isolation.test.js).
39
+ export async function ensureProjectManifest(projectRoot, packageManager) {
40
+ const pkgPath = path.join(projectRoot, 'package.json');
41
+ if (await fs.pathExists(pkgPath)) return pkgPath;
42
+
43
+ const pkg = {
44
+ name: path.basename(projectRoot),
45
+ version: '1.0.0',
46
+ description: 'Slice.js project',
47
+ main: 'api/index.js',
48
+ type: 'module',
49
+ engines: { node: '>=20.0.0' },
50
+ scripts: {}
51
+ };
52
+
53
+ // Persist the chosen package manager (corepack convention) so every later
54
+ // command — slice update, slice doctor — detects it deterministically.
55
+ const pmVersion = getPackageManagerVersion(packageManager);
56
+ if (pmVersion) {
57
+ pkg.packageManager = `${packageManager}@${pmVersion}`;
58
+ }
59
+
60
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
61
+ return pkgPath;
62
+ }
63
+
12
64
  // Visual components used by the App Shell + MultiRoute starter project.
13
65
  // We install only these on init; newcomers add more on demand with `slice get <Name>`.
14
66
  const STARTER_VISUAL_COMPONENTS = [
@@ -30,32 +82,52 @@ const STARTER_SERVICE_COMPONENTS = [
30
82
  'LocalStorageManager'
31
83
  ];
32
84
 
33
- export default async function initializeProject(projectType) {
85
+ export default async function initializeProject(options = {}) {
34
86
  try {
35
87
  const projectRoot = getProjectRoot(import.meta.url);
36
88
  const destinationApi = getApiPath(import.meta.url);
37
89
  const destinationSrc = getSrcPath(import.meta.url);
38
90
 
91
+ // Resolve the package manager chosen in `slice init` (or detect it when
92
+ // initializeProject is invoked directly, e.g. inside an existing folder).
93
+ const packageManager = options.packageManager
94
+ || resolvePackageManager(projectRoot).name;
95
+
96
+ // 0. CREATE PROJECT MANIFEST FIRST — must exist before any install so the
97
+ // package manager anchors node_modules inside the project folder.
98
+ await ensureProjectManifest(projectRoot, packageManager);
99
+
39
100
  const fwSpinner = ora('Ensuring latest Slice framework...').start();
40
101
  let latestVersion = null;
102
+ let installedVersion = null;
41
103
  let sliceBaseDir;
42
104
  try {
43
- const latest = execSync('npm view slicejs-web-framework version', { cwd: projectRoot }).toString().trim();
44
- latestVersion = latest;
105
+ latestVersion = await fetchLatestVersion('slicejs-web-framework');
45
106
  const installedPkgPath = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework', 'package.json');
46
107
  let installed = null;
47
108
  if (await fs.pathExists(installedPkgPath)) {
48
109
  const pkg = await fs.readJson(installedPkgPath);
49
110
  installed = pkg.version;
50
111
  }
51
- if (installed !== latest) {
52
- execSync(`npm install slicejs-web-framework@${latest} --save`, { cwd: projectRoot, stdio: 'inherit' });
112
+ if (!installed || (latestVersion && installed !== latestVersion)) {
113
+ // Install WITHOUT pinning an exact version: the package manager
114
+ // resolves it under its own policies (e.g. pnpm minimumReleaseAge
115
+ // quarantines versions younger than the configured age — pinning
116
+ // the registry's freshest version would make resolution fail).
117
+ execSync(installCommand(packageManager, 'slicejs-web-framework'), { cwd: projectRoot, stdio: 'inherit' });
118
+ }
119
+ if (await fs.pathExists(installedPkgPath)) {
120
+ const pkg = await fs.readJson(installedPkgPath);
121
+ installedVersion = pkg.version;
53
122
  }
54
123
  sliceBaseDir = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework');
55
- fwSpinner.succeed(`slicejs-web-framework@${latest} ready`);
124
+ fwSpinner.succeed(`slicejs-web-framework@${installedVersion || 'unknown'} ready`);
125
+ if (latestVersion && installedVersion && installedVersion !== latestVersion) {
126
+ Print.info(`Latest published is ${latestVersion}; your package manager resolved ${installedVersion} (release-age policy or cached registry).`);
127
+ }
56
128
  } catch (err) {
57
129
  // Fallback uses __dirname-style path because it looks for a local development copy,
58
- // not a project-relative path — npm install failed, so we fall back to monorepo sibling.
130
+ // not a project-relative path — the install failed, so we fall back to monorepo sibling.
59
131
  const fallback = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../slicejs-web-framework');
60
132
  if (await fs.pathExists(fallback)) {
61
133
  sliceBaseDir = fallback;
@@ -67,6 +139,21 @@ export default async function initializeProject(projectType) {
67
139
  }
68
140
  }
69
141
 
142
+ // 0b. INSTALL THE CLI LOCALLY (devDependency) so the generated scripts
143
+ // (`npm run dev` → `slice dev`) resolve via local delegation to a version
144
+ // pinned per project, as the docs recommend.
145
+ const cliSpinner = ora('Installing slicejs-cli as devDependency...').start();
146
+ try {
147
+ const cliPkgPath = getPath(import.meta.url, 'node_modules', 'slicejs-cli', 'package.json');
148
+ if (!(await fs.pathExists(cliPkgPath))) {
149
+ execSync(installCommand(packageManager, 'slicejs-cli', { dev: true }), { cwd: projectRoot, stdio: 'inherit' });
150
+ }
151
+ cliSpinner.succeed('slicejs-cli installed locally');
152
+ } catch (err) {
153
+ cliSpinner.warn('Could not install slicejs-cli locally — scripts will use the global CLI');
154
+ Print.info(`You can add it later with: ${installCommand(packageManager, 'slicejs-cli', { dev: true })}`);
155
+ }
156
+
70
157
  // These derive from sliceBaseDir (which comes from npm install or fallback),
71
158
  // so they're already dynamic — no PathHelper needed.
72
159
  const apiDir = path.join(sliceBaseDir, 'api');
@@ -245,6 +332,7 @@ export default async function initializeProject(projectType) {
245
332
 
246
333
  // Comandos principales
247
334
  pkg.scripts['dev'] = 'slice dev';
335
+ pkg.scripts['build'] = 'slice build';
248
336
  pkg.scripts['start'] = 'slice start';
249
337
 
250
338
  // Component management
@@ -257,39 +345,28 @@ export default async function initializeProject(projectType) {
257
345
  pkg.scripts['browse'] = 'slice browse';
258
346
  pkg.scripts['sync'] = 'slice sync';
259
347
 
260
- // Utilidades
261
- pkg.scripts['slice:version'] = 'slice version';
262
- pkg.scripts['slice:update'] = 'slice update';
263
- pkg.scripts['slice:types'] = 'slice types generate';
264
-
265
- // Legacy (compatibility)
266
- pkg.scripts['slice:init'] = 'slice init';
267
- pkg.scripts['slice:start'] = 'slice start';
268
- pkg.scripts['slice:dev'] = 'slice dev';
269
- pkg.scripts['slice:create'] = 'slice component create';
270
- pkg.scripts['slice:list'] = 'slice component list';
271
- pkg.scripts['slice:delete'] = 'slice component delete';
272
- pkg.scripts['slice:get'] = 'slice get';
273
- pkg.scripts['slice:browse'] = 'slice browse';
274
- pkg.scripts['slice:sync'] = 'slice sync';
348
+ // slice:* namespaced set — shared with post.js and `slice postinstall`
349
+ // (commands/utils/sliceScripts.js) so the three never drift apart.
350
+ Object.assign(pkg.scripts, SLICE_SCRIPTS);
275
351
  pkg.scripts['run'] = 'slice dev';
276
352
 
277
353
  // Module configuration
278
354
  pkg.type = 'module';
279
355
  pkg.engines = pkg.engines || { node: '>=20.0.0' };
280
356
 
281
- // Ensure framework dependency is present
357
+ // Ensure framework dependency is present (the install above normally
358
+ // already wrote it; this is a fallback for the monorepo-sibling path).
282
359
  if (!pkg.dependencies['slicejs-web-framework']) {
283
- pkg.dependencies['slicejs-web-framework'] = latestVersion ? latestVersion : 'latest';
360
+ pkg.dependencies['slicejs-web-framework'] = installedVersion ? `^${installedVersion}` : 'latest';
284
361
  }
285
362
 
286
363
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
287
- pkgSpinner.succeed('npm scripts configured successfully');
364
+ pkgSpinner.succeed('Package scripts configured successfully');
288
365
 
289
366
  Print.title('New recommended commands:');
290
- console.log(' npm run dev - Start development server');
291
- console.log(' npm run get - Install components');
292
- console.log(' npm run browse - Browse components');
367
+ console.log(` ${packageManager} run dev - Start development server`);
368
+ console.log(` ${packageManager} run get - Install components`);
369
+ console.log(` ${packageManager} run browse - Browse components`);
293
370
  } catch (error) {
294
371
  pkgSpinner.fail('Failed to configure npm scripts');
295
372
  Print.error(error.message);
@@ -300,6 +377,7 @@ export default async function initializeProject(projectType) {
300
377
  Print.newLine();
301
378
  Print.title('Next steps:');
302
379
  console.log(` cd ${projectName}`);
380
+ console.log(` ${packageManager} run dev - Start development server`);
303
381
  console.log(' slice browse - View available components');
304
382
  console.log(' slice get Button - Install specific components');
305
383
  console.log(' slice sync - Update all components to latest versions');