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
@@ -5,10 +5,107 @@ 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
+ async function ensurePnpmAllowBuilds(projectRoot) {
36
+ const workspacePath = path.join(projectRoot, 'pnpm-workspace.yaml');
37
+ const allowBuildLine = ' slicejs-cli: true';
38
+
39
+ if (!(await fs.pathExists(workspacePath))) {
40
+ await fs.writeFile(workspacePath, `allowBuilds:\n${allowBuildLine}\n`, 'utf8');
41
+ return;
42
+ }
43
+
44
+ const raw = await fs.readFile(workspacePath, 'utf8');
45
+ const lines = raw.split(/\r?\n/);
46
+ const allowIdx = lines.findIndex((line) => /^allowBuilds:\s*$/.test(line));
47
+
48
+ if (allowIdx === -1) {
49
+ const suffix = raw.endsWith('\n') ? '' : '\n';
50
+ await fs.writeFile(workspacePath, `${raw}${suffix}allowBuilds:\n${allowBuildLine}\n`, 'utf8');
51
+ return;
52
+ }
53
+
54
+ let blockEnd = lines.length;
55
+ for (let i = allowIdx + 1; i < lines.length; i++) {
56
+ const line = lines[i];
57
+ if (!line.trim()) continue;
58
+ if (!/^\s/.test(line)) {
59
+ blockEnd = i;
60
+ break;
61
+ }
62
+ }
63
+
64
+ let found = false;
65
+ for (let i = allowIdx + 1; i < blockEnd; i++) {
66
+ if (/^\s+slicejs-cli\s*:/.test(lines[i])) {
67
+ lines[i] = allowBuildLine;
68
+ found = true;
69
+ break;
70
+ }
71
+ }
72
+
73
+ if (!found) {
74
+ lines.splice(blockEnd, 0, allowBuildLine);
75
+ }
76
+
77
+ await fs.writeFile(workspacePath, `${lines.join('\n').replace(/\n*$/, '\n')}`, 'utf8');
78
+ }
79
+
80
+ // Create the project manifest BEFORE any install runs. Without a package.json in
81
+ // the project folder, npm/pnpm walk up the directory tree looking for the nearest
82
+ // manifest and anchor node_modules (and the dependency entry) OUTSIDE the project.
83
+ // Exported for tests (init-project-isolation.test.js).
84
+ export async function ensureProjectManifest(projectRoot, packageManager) {
85
+ const pkgPath = path.join(projectRoot, 'package.json');
86
+ if (await fs.pathExists(pkgPath)) return pkgPath;
87
+
88
+ const pkg = {
89
+ name: path.basename(projectRoot),
90
+ version: '1.0.0',
91
+ description: 'Slice.js project',
92
+ main: 'api/index.js',
93
+ type: 'module',
94
+ engines: { node: '>=20.0.0' },
95
+ scripts: {}
96
+ };
97
+
98
+ // Persist the chosen package manager (corepack convention) so every later
99
+ // command — slice update, slice doctor — detects it deterministically.
100
+ const pmVersion = getPackageManagerVersion(packageManager);
101
+ if (pmVersion) {
102
+ pkg.packageManager = `${packageManager}@${pmVersion}`;
103
+ }
104
+
105
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
106
+ return pkgPath;
107
+ }
108
+
12
109
  // Visual components used by the App Shell + MultiRoute starter project.
13
110
  // We install only these on init; newcomers add more on demand with `slice get <Name>`.
14
111
  const STARTER_VISUAL_COMPONENTS = [
@@ -30,32 +127,56 @@ const STARTER_SERVICE_COMPONENTS = [
30
127
  'LocalStorageManager'
31
128
  ];
32
129
 
33
- export default async function initializeProject(projectType) {
130
+ export default async function initializeProject(options = {}) {
34
131
  try {
35
132
  const projectRoot = getProjectRoot(import.meta.url);
36
133
  const destinationApi = getApiPath(import.meta.url);
37
134
  const destinationSrc = getSrcPath(import.meta.url);
38
135
 
136
+ // Resolve the package manager chosen in `slice init` (or detect it when
137
+ // initializeProject is invoked directly, e.g. inside an existing folder).
138
+ const packageManager = options.packageManager
139
+ || resolvePackageManager(projectRoot).name;
140
+
141
+ if (packageManager === 'pnpm') {
142
+ await ensurePnpmAllowBuilds(projectRoot);
143
+ }
144
+
145
+ // 0. CREATE PROJECT MANIFEST FIRST — must exist before any install so the
146
+ // package manager anchors node_modules inside the project folder.
147
+ await ensureProjectManifest(projectRoot, packageManager);
148
+
39
149
  const fwSpinner = ora('Ensuring latest Slice framework...').start();
40
150
  let latestVersion = null;
151
+ let installedVersion = null;
41
152
  let sliceBaseDir;
42
153
  try {
43
- const latest = execSync('npm view slicejs-web-framework version', { cwd: projectRoot }).toString().trim();
44
- latestVersion = latest;
154
+ latestVersion = await fetchLatestVersion('slicejs-web-framework');
45
155
  const installedPkgPath = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework', 'package.json');
46
156
  let installed = null;
47
157
  if (await fs.pathExists(installedPkgPath)) {
48
158
  const pkg = await fs.readJson(installedPkgPath);
49
159
  installed = pkg.version;
50
160
  }
51
- if (installed !== latest) {
52
- execSync(`npm install slicejs-web-framework@${latest} --save`, { cwd: projectRoot, stdio: 'inherit' });
161
+ if (!installed || (latestVersion && installed !== latestVersion)) {
162
+ // Install WITHOUT pinning an exact version: the package manager
163
+ // resolves it under its own policies (e.g. pnpm minimumReleaseAge
164
+ // quarantines versions younger than the configured age — pinning
165
+ // the registry's freshest version would make resolution fail).
166
+ execSync(installCommand(packageManager, 'slicejs-web-framework'), { cwd: projectRoot, stdio: 'inherit' });
167
+ }
168
+ if (await fs.pathExists(installedPkgPath)) {
169
+ const pkg = await fs.readJson(installedPkgPath);
170
+ installedVersion = pkg.version;
53
171
  }
54
172
  sliceBaseDir = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework');
55
- fwSpinner.succeed(`slicejs-web-framework@${latest} ready`);
173
+ fwSpinner.succeed(`slicejs-web-framework@${installedVersion || 'unknown'} ready`);
174
+ if (latestVersion && installedVersion && installedVersion !== latestVersion) {
175
+ Print.info(`Latest published is ${latestVersion}; your package manager resolved ${installedVersion} (release-age policy or cached registry).`);
176
+ }
56
177
  } catch (err) {
57
178
  // 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.
179
+ // not a project-relative path — the install failed, so we fall back to monorepo sibling.
59
180
  const fallback = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../slicejs-web-framework');
60
181
  if (await fs.pathExists(fallback)) {
61
182
  sliceBaseDir = fallback;
@@ -67,6 +188,21 @@ export default async function initializeProject(projectType) {
67
188
  }
68
189
  }
69
190
 
191
+ // 0b. INSTALL THE CLI LOCALLY (devDependency) so the generated scripts
192
+ // (`npm run dev` → `slice dev`) resolve via local delegation to a version
193
+ // pinned per project, as the docs recommend.
194
+ const cliSpinner = ora('Installing slicejs-cli as devDependency...').start();
195
+ try {
196
+ const cliPkgPath = getPath(import.meta.url, 'node_modules', 'slicejs-cli', 'package.json');
197
+ if (!(await fs.pathExists(cliPkgPath))) {
198
+ execSync(installCommand(packageManager, 'slicejs-cli', { dev: true }), { cwd: projectRoot, stdio: 'inherit' });
199
+ }
200
+ cliSpinner.succeed('slicejs-cli installed locally');
201
+ } catch (err) {
202
+ cliSpinner.warn('Could not install slicejs-cli locally — scripts will use the global CLI');
203
+ Print.info(`You can add it later with: ${installCommand(packageManager, 'slicejs-cli', { dev: true })}`);
204
+ }
205
+
70
206
  // These derive from sliceBaseDir (which comes from npm install or fallback),
71
207
  // so they're already dynamic — no PathHelper needed.
72
208
  const apiDir = path.join(sliceBaseDir, 'api');
@@ -169,20 +305,20 @@ export default async function initializeProject(projectType) {
169
305
  componentsSpinner.succeed(`All ${successful} Visual components installed successfully`);
170
306
  } else if (successful > 0) {
171
307
  componentsSpinner.warn(`${successful} components installed, ${failed} failed`);
172
- Print.info('You can install failed components later using "slice get <component-name>"');
308
+ Print.info(`You can install failed components later using "${packageManager} run get -- <component-name>"`);
173
309
  } else {
174
310
  componentsSpinner.fail('Failed to install Visual components');
175
311
  }
176
312
  } else {
177
313
  componentsSpinner.warn('No Visual components found in registry');
178
- Print.info('You can add components later using "slice get <component-name>"');
314
+ Print.info(`You can add components later using "${packageManager} run get -- <component-name>"`);
179
315
  }
180
316
 
181
317
  } catch (error) {
182
318
  componentsSpinner.fail('Could not download Visual components from official repository');
183
319
  Print.error(`Repository error: ${error.message}`);
184
320
  Print.info('Project initialized without Visual components');
185
- Print.info('You can add them later using "slice get <component-name>"');
321
+ Print.info(`You can add them later using "${packageManager} run get -- <component-name>"`);
186
322
  }
187
323
 
188
324
  // 3b. DOWNLOAD STARTER SERVICE COMPONENTS FROM OFFICIAL REPOSITORY
@@ -208,7 +344,7 @@ export default async function initializeProject(projectType) {
208
344
  serviceSpinner.succeed(`All ${successful} Service components installed successfully`);
209
345
  } else if (successful > 0) {
210
346
  serviceSpinner.warn(`${successful} Service components installed, ${failed} failed`);
211
- Print.info('You can install failed components later using "slice get <component-name>"');
347
+ Print.info(`You can install failed components later using "${packageManager} run get -- <component-name>"`);
212
348
  } else {
213
349
  serviceSpinner.fail('Failed to install Service components');
214
350
  }
@@ -218,7 +354,7 @@ export default async function initializeProject(projectType) {
218
354
  } catch (error) {
219
355
  serviceSpinner.fail('Could not download Service components from official repository');
220
356
  Print.error(`Repository error: ${error.message}`);
221
- Print.info('You can add them later using "slice get <component-name>"');
357
+ Print.info(`You can add them later using "${packageManager} run get -- <component-name>"`);
222
358
  }
223
359
 
224
360
  // 4. CONFIGURE SCRIPTS IN PROJECT package.json
@@ -243,53 +379,43 @@ export default async function initializeProject(projectType) {
243
379
  pkg.scripts = pkg.scripts || {};
244
380
  pkg.dependencies = pkg.dependencies || {};
245
381
 
246
- // Comandos principales
247
- pkg.scripts['dev'] = 'slice dev';
248
- pkg.scripts['start'] = 'slice start';
382
+ // Main scripts (local CLI path, no global launcher dependency)
383
+ pkg.scripts['dev'] = SLICE_SCRIPTS['slice:dev'];
384
+ pkg.scripts['build'] = SLICE_SCRIPTS['slice:build'];
385
+ pkg.scripts['start'] = SLICE_SCRIPTS['slice:start'];
249
386
 
250
387
  // Component management
251
- pkg.scripts['component:create'] = 'slice component create';
252
- pkg.scripts['component:list'] = 'slice component list';
253
- pkg.scripts['component:delete'] = 'slice component delete';
254
-
255
- // Atajos de repositorio
256
- pkg.scripts['get'] = 'slice get';
257
- pkg.scripts['browse'] = 'slice browse';
258
- pkg.scripts['sync'] = 'slice sync';
259
-
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';
275
- pkg.scripts['run'] = 'slice dev';
388
+ pkg.scripts['component:create'] = SLICE_SCRIPTS['slice:create'];
389
+ pkg.scripts['component:list'] = SLICE_SCRIPTS['slice:list'];
390
+ pkg.scripts['component:delete'] = SLICE_SCRIPTS['slice:delete'];
391
+
392
+ // Registry shortcuts
393
+ pkg.scripts['get'] = SLICE_SCRIPTS['slice:get'];
394
+ pkg.scripts['browse'] = SLICE_SCRIPTS['slice:browse'];
395
+ pkg.scripts['sync'] = SLICE_SCRIPTS['slice:sync'];
396
+
397
+ // slice:* namespaced set — shared with post.js and `slice postinstall`
398
+ // (commands/utils/sliceScripts.js) so the three never drift apart.
399
+ Object.assign(pkg.scripts, SLICE_SCRIPTS);
400
+ pkg.scripts['run'] = SLICE_SCRIPTS['slice:dev'];
276
401
 
277
402
  // Module configuration
278
403
  pkg.type = 'module';
279
404
  pkg.engines = pkg.engines || { node: '>=20.0.0' };
280
405
 
281
- // Ensure framework dependency is present
406
+ // Ensure framework dependency is present (the install above normally
407
+ // already wrote it; this is a fallback for the monorepo-sibling path).
282
408
  if (!pkg.dependencies['slicejs-web-framework']) {
283
- pkg.dependencies['slicejs-web-framework'] = latestVersion ? latestVersion : 'latest';
409
+ pkg.dependencies['slicejs-web-framework'] = installedVersion ? `^${installedVersion}` : 'latest';
284
410
  }
285
411
 
286
412
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
287
- pkgSpinner.succeed('npm scripts configured successfully');
413
+ pkgSpinner.succeed('Package scripts configured successfully');
288
414
 
289
415
  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');
416
+ console.log(` ${packageManager} run dev - Start development server`);
417
+ console.log(` ${packageManager} run get - Install components`);
418
+ console.log(` ${packageManager} run browse - Browse components`);
293
419
  } catch (error) {
294
420
  pkgSpinner.fail('Failed to configure npm scripts');
295
421
  Print.error(error.message);
@@ -300,9 +426,10 @@ export default async function initializeProject(projectType) {
300
426
  Print.newLine();
301
427
  Print.title('Next steps:');
302
428
  console.log(` cd ${projectName}`);
303
- console.log(' slice browse - View available components');
304
- console.log(' slice get Button - Install specific components');
305
- console.log(' slice sync - Update all components to latest versions');
429
+ console.log(` ${packageManager} run dev - Start development server`);
430
+ console.log(` ${packageManager} run browse - View available components`);
431
+ console.log(` ${packageManager} run get -- Button - Install specific components`);
432
+ console.log(` ${packageManager} run sync - Update all components to latest versions`);
306
433
 
307
434
  } catch (error) {
308
435
  Print.error('Unexpected error initializing project:', error.message);
@@ -0,0 +1,148 @@
1
+ // commands/utils/PackageManager.js
2
+ //
3
+ // Package manager detection and command building (pnpm / npm).
4
+ // Resolution priority for an existing project:
5
+ // 1. "packageManager" field in the project package.json (corepack convention)
6
+ // 2. Lockfile present at the project root
7
+ // 3. npm_config_user_agent (set when the CLI runs via `npx` / `pnpm dlx` / a PM script)
8
+ // 4. The only PM binary available on PATH (if exactly one)
9
+ // When everything is ambiguous, detectPackageManager() returns null so callers
10
+ // can prompt the user (interactive init) or fall back to pnpm (non-interactive).
11
+
12
+ import fs from 'fs-extra'
13
+ import path from 'path'
14
+ import { spawnSync } from 'node:child_process'
15
+
16
+ export const SUPPORTED_PACKAGE_MANAGERS = ['pnpm', 'npm']
17
+
18
+ const LOCKFILES = {
19
+ 'pnpm-lock.yaml': 'pnpm',
20
+ 'package-lock.json': 'npm'
21
+ }
22
+
23
+ export function parseUserAgent(userAgent = process.env.npm_config_user_agent) {
24
+ if (!userAgent) return null
25
+ const match = userAgent.match(/^(npm|pnpm)\/(\S+)/)
26
+ if (!match) return null
27
+ return { name: match[1], version: match[2], source: 'user-agent' }
28
+ }
29
+
30
+ export function fromPackageManagerField(projectRoot) {
31
+ try {
32
+ const pkgPath = path.join(projectRoot, 'package.json')
33
+ if (!fs.pathExistsSync(pkgPath)) return null
34
+ const pkg = fs.readJsonSync(pkgPath)
35
+ if (typeof pkg.packageManager !== 'string') return null
36
+ const match = pkg.packageManager.match(/^(npm|pnpm)@(\S+)/)
37
+ if (!match) return null
38
+ return { name: match[1], version: match[2], source: 'package-manager-field' }
39
+ } catch {
40
+ return null
41
+ }
42
+ }
43
+
44
+ export function fromLockfile(projectRoot) {
45
+ for (const [lockfile, name] of Object.entries(LOCKFILES)) {
46
+ if (fs.pathExistsSync(path.join(projectRoot, lockfile))) {
47
+ return { name, version: null, source: `lockfile (${lockfile})` }
48
+ }
49
+ }
50
+ return null
51
+ }
52
+
53
+ function runPmBinary(name, args) {
54
+ // On Windows the PM entry points are .cmd shims, which require shell resolution.
55
+ const isWindows = process.platform === 'win32'
56
+ return spawnSync(name, args, {
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ shell: isWindows,
59
+ encoding: 'utf-8'
60
+ })
61
+ }
62
+
63
+ export function isPackageManagerAvailable(name) {
64
+ try {
65
+ const result = runPmBinary(name, ['--version'])
66
+ return result.status === 0
67
+ } catch {
68
+ return false
69
+ }
70
+ }
71
+
72
+ export function getPackageManagerVersion(name) {
73
+ try {
74
+ const result = runPmBinary(name, ['--version'])
75
+ if (result.status !== 0) return null
76
+ return (result.stdout || '').toString().trim() || null
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
82
+ export function getAvailablePackageManagers() {
83
+ return SUPPORTED_PACKAGE_MANAGERS.filter(isPackageManagerAvailable)
84
+ }
85
+
86
+ /**
87
+ * Detect the package manager for a project.
88
+ * Returns { name, version, source } or null when genuinely ambiguous.
89
+ */
90
+ export function detectPackageManager(projectRoot, { userAgent = process.env.npm_config_user_agent } = {}) {
91
+ if (projectRoot) {
92
+ const fromField = fromPackageManagerField(projectRoot)
93
+ if (fromField) return fromField
94
+
95
+ const fromLock = fromLockfile(projectRoot)
96
+ if (fromLock) return fromLock
97
+ }
98
+
99
+ const fromUa = parseUserAgent(userAgent)
100
+ if (fromUa) return fromUa
101
+
102
+ const available = getAvailablePackageManagers()
103
+ if (available.length === 1) {
104
+ return { name: available[0], version: null, source: 'only available binary' }
105
+ }
106
+
107
+ return null
108
+ }
109
+
110
+ /**
111
+ * Non-interactive resolution: detection first, then pnpm if available,
112
+ * then whatever binary exists. Never returns null so update/doctor flows
113
+ * always have a usable PM name (commands fail later with a clear error
114
+ * if no PM is actually installed).
115
+ */
116
+ export function resolvePackageManager(projectRoot, options = {}) {
117
+ const detected = detectPackageManager(projectRoot, options)
118
+ if (detected) return detected
119
+
120
+ const available = getAvailablePackageManagers()
121
+ if (available.includes('pnpm')) return { name: 'pnpm', version: null, source: 'fallback' }
122
+ if (available.includes('npm')) return { name: 'npm', version: null, source: 'fallback' }
123
+ if (available.length > 0) return { name: available[0], version: null, source: 'fallback' }
124
+ return { name: 'pnpm', version: null, source: 'fallback (none detected)' }
125
+ }
126
+
127
+ export function installCommand(pmName, packages, { dev = false, global: isGlobal = false } = {}) {
128
+ const pkgs = Array.isArray(packages) ? packages.join(' ') : packages
129
+ const flags = [isGlobal ? '-g' : '', dev ? '-D' : ''].filter(Boolean).join(' ')
130
+ const flagSuffix = flags ? ` ${flags}` : ''
131
+ if (pmName === 'pnpm') {
132
+ return `${pmName} add${flagSuffix} ${pkgs}`
133
+ }
134
+ return `npm install${flagSuffix} ${pkgs}`
135
+ }
136
+
137
+ export function uninstallCommand(pmName, packages, { global: isGlobal = false } = {}) {
138
+ const pkgs = Array.isArray(packages) ? packages.join(' ') : packages
139
+ const flagSuffix = isGlobal ? ' -g' : ''
140
+ if (pmName === 'pnpm') {
141
+ return `${pmName} remove${flagSuffix} ${pkgs}`
142
+ }
143
+ return `npm uninstall${flagSuffix} ${pkgs}`
144
+ }
145
+
146
+ export function runScriptCommand(pmName, script) {
147
+ return `${pmName} run ${script}`
148
+ }
@@ -5,6 +5,7 @@ import path from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import Print from "../Print.js";
7
7
  import { getProjectRoot, getPath } from "../utils/PathHelper.js";
8
+ import { resolvePackageManager } from "../utils/PackageManager.js";
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
 
@@ -110,17 +111,18 @@ class VersionChecker {
110
111
  const frameworkStatus = this.compareVersions(current.framework, latest.framework);
111
112
 
112
113
  if (!silent && (cliStatus === 'outdated' || frameworkStatus === 'outdated')) {
114
+ const pm = resolvePackageManager(getProjectRoot(import.meta.url)).name;
113
115
  console.log(''); // Line break
114
116
  Print.warning('📦 Available Updates:');
115
-
117
+
116
118
  if (cliStatus === 'outdated') {
117
119
  console.log(` 🔧 CLI: ${current.cli} → ${latest.cli}`);
118
- console.log(` npm update slicejs-cli`);
120
+ console.log(` ${pm} update slicejs-cli`);
119
121
  }
120
-
122
+
121
123
  if (frameworkStatus === 'outdated') {
122
124
  console.log(` ⚡ Framework: ${current.framework} → ${latest.framework}`);
123
- console.log(` npm update slicejs-web-framework`);
125
+ console.log(` ${pm} update slicejs-web-framework`);
124
126
  }
125
127
 
126
128
  console.log(' 📚 Changelog: https://github.com/VKneider/slice.js/releases');
@@ -0,0 +1,23 @@
1
+ // commands/utils/sliceScripts.js
2
+ //
3
+ // Single source of truth for the slice:* package scripts configured by the CLI.
4
+ // Used by post.js (the postinstall hook), the `slice postinstall` command in
5
+ // client.js, and `slice init` — so the three can never drift apart (they did:
6
+ // client.js was missing slice:types, and none of them had slice:build).
7
+ export const SLICE_SCRIPTS = {
8
+ 'slice:init': 'node ./node_modules/slicejs-cli/client.js init',
9
+ 'slice:dev': 'node ./node_modules/slicejs-cli/client.js dev',
10
+ 'slice:build': 'node ./node_modules/slicejs-cli/client.js build',
11
+ 'slice:start': 'node ./node_modules/slicejs-cli/client.js start',
12
+ 'slice:create': 'node ./node_modules/slicejs-cli/client.js component create',
13
+ 'slice:list': 'node ./node_modules/slicejs-cli/client.js component list',
14
+ 'slice:delete': 'node ./node_modules/slicejs-cli/client.js component delete',
15
+ 'slice:get': 'node ./node_modules/slicejs-cli/client.js get',
16
+ 'slice:browse': 'node ./node_modules/slicejs-cli/client.js browse',
17
+ 'slice:sync': 'node ./node_modules/slicejs-cli/client.js sync',
18
+ 'slice:doctor': 'node ./node_modules/slicejs-cli/client.js doctor',
19
+ 'slice:version': 'node ./node_modules/slicejs-cli/client.js version',
20
+ 'slice:help': 'node ./node_modules/slicejs-cli/client.js --help',
21
+ 'slice:update': 'node ./node_modules/slicejs-cli/client.js update',
22
+ 'slice:types': 'node ./node_modules/slicejs-cli/client.js types generate',
23
+ };