slicejs-cli 3.5.0 → 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 (39) hide show
  1. package/README.md +34 -15
  2. package/client.js +67 -20
  3. package/commands/createComponent/createComponent.js +6 -2
  4. package/commands/deleteComponent/deleteComponent.js +4 -0
  5. package/commands/doctor/doctor.js +78 -3
  6. package/commands/getComponent/getComponent.js +33 -25
  7. package/commands/init/init.js +106 -28
  8. package/commands/utils/PackageManager.js +148 -0
  9. package/commands/utils/VersionChecker.js +6 -4
  10. package/commands/utils/bundling/BundleGenerator.js +271 -38
  11. package/commands/utils/sliceScripts.js +21 -0
  12. package/commands/utils/updateManager.js +54 -35
  13. package/package.json +15 -1
  14. package/post.js +8 -16
  15. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  17. package/.github/pull_request_template.md +0 -22
  18. package/AGENTS.md +0 -247
  19. package/CODE_OF_CONDUCT.md +0 -126
  20. package/ECOSYSTEM.md +0 -9
  21. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +0 -182
  22. package/tests/bundle-generator.test.js +0 -691
  23. package/tests/bundle-v2-register-output.test.js +0 -470
  24. package/tests/client-launcher-contract.test.js +0 -211
  25. package/tests/client-update-flow-contract.test.js +0 -272
  26. package/tests/component-registry-parse.test.js +0 -34
  27. package/tests/dependency-analyzer.test.js +0 -24
  28. package/tests/fixtures/components.js +0 -8
  29. package/tests/fixtures/sliceConfig.json +0 -74
  30. package/tests/getcomponent.test.js +0 -407
  31. package/tests/helpers/setup.js +0 -97
  32. package/tests/init-command-contract.test.js +0 -46
  33. package/tests/local-cli-delegation.test.js +0 -81
  34. package/tests/path-helper.test.js +0 -206
  35. package/tests/postinstall-command.test.js +0 -72
  36. package/tests/types-breakage.test.js +0 -491
  37. package/tests/types-generator-errors.test.js +0 -361
  38. package/tests/types-generator.test.js +0 -344
  39. package/tests/update-manager-notifications.test.js +0 -88
@@ -1249,7 +1249,11 @@ export default class BundleGenerator {
1249
1249
  const omittedDependencies = options.omittedDependencies instanceof Set
1250
1250
  ? options.omittedDependencies
1251
1251
  : new Set(options.omittedDependencies || []);
1252
- const filteredModules = modules.filter((module) => !omittedDependencies.has(module.name));
1252
+ // Emit in topological order so a module is registered before any module
1253
+ // that depends on it (its transitive imports resolve at IIFE-eval time).
1254
+ const filteredModules = this.sortDependencyModulesTopologically(
1255
+ modules.filter((module) => !omittedDependencies.has(module.name))
1256
+ );
1253
1257
 
1254
1258
  const lines = [
1255
1259
  'const SLICE_BUNDLE_DEPENDENCIES = {};',
@@ -1260,32 +1264,88 @@ export default class BundleGenerator {
1260
1264
  }
1261
1265
  filteredModules.forEach((module, index) => {
1262
1266
  const exportVar = `__sliceDepExports${index}`;
1263
- const transformedContent = this.transformDependencyContent(module.content, exportVar, module.name);
1264
- lines.push(`const ${exportVar} = {};`);
1265
- lines.push(transformedContent.trim());
1267
+ // Evaluate each dependency inside its own IIFE so its private,
1268
+ // non-exported top-level bindings stay local and cannot collide with
1269
+ // another dependency's (or the bundle's) identifiers. Only the exports
1270
+ // object escapes the closure.
1271
+ const transformedContent = this.transformDependencyContent(module.content, '__sliceExports', module.name);
1272
+ // Bind this module's own (transitive) imports inside its IIFE — they were
1273
+ // registered by earlier modules in the topological order.
1274
+ const importBindings = this.buildDependencyBindings(
1275
+ Object.fromEntries(
1276
+ (module.moduleImports || [])
1277
+ .filter((mi) => mi.bindings && mi.bindings.length)
1278
+ .map((mi) => [mi.depName, { bindings: mi.bindings }])
1279
+ ),
1280
+ { preferShared: !!options.includeSharedResolver }
1281
+ );
1282
+ const body = transformedContent.trim();
1283
+ lines.push(`const ${exportVar} = (() => {`);
1284
+ lines.push('const __sliceExports = {};');
1285
+ if (importBindings) lines.push(importBindings);
1286
+ if (body) lines.push(body);
1287
+ lines.push('return __sliceExports;');
1288
+ lines.push('})();');
1266
1289
  lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1267
1290
  });
1268
1291
 
1269
1292
  return lines.join('\n');
1270
1293
  }
1271
1294
 
1295
+ sortDependencyModulesTopologically(modules = []) {
1296
+ const byName = new Map(modules.map((module) => [module.name, module]));
1297
+ const visited = new Set();
1298
+ const ordered = [];
1299
+ const visit = (module, stack) => {
1300
+ if (visited.has(module.name) || stack.has(module.name)) return;
1301
+ stack.add(module.name);
1302
+ for (const imp of module.moduleImports || []) {
1303
+ const dependency = byName.get(imp.depName);
1304
+ if (dependency) visit(dependency, stack);
1305
+ }
1306
+ stack.delete(module.name);
1307
+ visited.add(module.name);
1308
+ ordered.push(module);
1309
+ };
1310
+ for (const module of modules) visit(module, new Set());
1311
+ return ordered;
1312
+ }
1313
+
1272
1314
  async buildDependencyContents(jsContent, componentPath) {
1273
- const dependencies = this.analyzeDependencies(jsContent, componentPath);
1274
1315
  const dependencyContents = {};
1316
+ const visited = new Set();
1275
1317
 
1276
- for (const dep of dependencies) {
1277
- const depPath = dep.path;
1278
- try {
1279
- const depContent = await fs.readFile(depPath, 'utf-8');
1280
- const depName = path
1281
- .relative(this.srcPath, depPath)
1282
- .replace(/\\/g, '/');
1283
- dependencyContents[depName] = {
1284
- content: depContent,
1285
- bindings: dep.bindings || []
1286
- };
1287
- } catch (error) {
1288
- console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
1318
+ // Recursively resolve the relative-import graph rooted at `content`, so a
1319
+ // dependency module's OWN (transitive) imports are inlined too. Returns the
1320
+ // consumer's direct imports as [{ depName, bindings }].
1321
+ const resolveModule = async (content, basePath) => {
1322
+ const consumerImports = [];
1323
+
1324
+ for (const dep of this.analyzeDependencies(content, basePath)) {
1325
+ const depName = path.relative(this.srcPath, dep.path).replace(/\\/g, '/');
1326
+ consumerImports.push({ depName, bindings: dep.bindings || [] });
1327
+
1328
+ if (visited.has(depName)) continue;
1329
+ visited.add(depName);
1330
+
1331
+ try {
1332
+ const depContent = await fs.readFile(dep.path, 'utf-8');
1333
+ // Resolve this module's own transitive imports first.
1334
+ const moduleImports = await resolveModule(depContent, path.dirname(dep.path));
1335
+ dependencyContents[depName] = { content: depContent, bindings: [], moduleImports };
1336
+ } catch (error) {
1337
+ console.warn(`Warning: Could not read dependency ${dep.path}:`, error.message);
1338
+ }
1339
+ }
1340
+
1341
+ return consumerImports;
1342
+ };
1343
+
1344
+ const directImports = await resolveModule(jsContent, componentPath);
1345
+ // The component's direct imports drive its class-factory bindings.
1346
+ for (const { depName, bindings } of directImports) {
1347
+ if (dependencyContents[depName]) {
1348
+ dependencyContents[depName].bindings = bindings;
1289
1349
  }
1290
1350
  }
1291
1351
 
@@ -1428,10 +1488,17 @@ if (window.slice && window.slice.controller) {
1428
1488
 
1429
1489
  dependencyModules.forEach((module, index) => {
1430
1490
  const exportVar = `__sliceDepExports${index}`;
1431
- const content = this.transformDependencyContent(module.content, exportVar, module.name);
1491
+ // Each dependency lives in its own IIFE scope (see
1492
+ // buildV2DependencyModuleBlockFromModules) so private helpers cannot
1493
+ // collide across modules.
1494
+ const content = this.transformDependencyContent(module.content, '__sliceExports', module.name);
1495
+ const body = content.trim();
1432
1496
  lines.push(`// Dependency: ${module.name}`);
1433
- lines.push(`const ${exportVar} = {};`);
1434
- lines.push(content.trim());
1497
+ lines.push(`const ${exportVar} = (() => {`);
1498
+ lines.push('const __sliceExports = {};');
1499
+ if (body) lines.push(body);
1500
+ lines.push('return __sliceExports;');
1501
+ lines.push('})();');
1435
1502
  lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
1436
1503
  });
1437
1504
 
@@ -1458,7 +1525,8 @@ if (window.slice && window.slice.controller) {
1458
1525
  if (modules.has(moduleName)) continue;
1459
1526
  const content = typeof entry === 'string' ? entry : entry?.content;
1460
1527
  if (!content) continue;
1461
- modules.set(moduleName, { name: moduleName, content });
1528
+ const moduleImports = (entry && typeof entry === 'object' ? entry.moduleImports : null) || [];
1529
+ modules.set(moduleName, { name: moduleName, content, moduleImports });
1462
1530
  }
1463
1531
  }
1464
1532
  return Array.from(modules.values());
@@ -1469,6 +1537,158 @@ if (window.slice && window.slice.controller) {
1469
1537
  }
1470
1538
 
1471
1539
  transformDependencyContent(content, exportVar, moduleName) {
1540
+ let ast;
1541
+ try {
1542
+ ast = parse(content, { sourceType: 'module', plugins: ['jsx'] });
1543
+ } catch (error) {
1544
+ // Unparseable content (e.g. TS syntax): fall back to the regex transform
1545
+ // so we never lose a dependency entirely.
1546
+ return this.transformDependencyContentRegexFallback(content, exportVar, moduleName);
1547
+ }
1548
+
1549
+ const fallbackKey = this.getDependencyDefaultFallbackKey(moduleName);
1550
+ const statements = ast.program.body
1551
+ .filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
1552
+ .sort((a, b) => a.start - b.start);
1553
+
1554
+ let cursor = 0;
1555
+ let output = '';
1556
+ for (const node of statements) {
1557
+ output += content.slice(cursor, node.start);
1558
+ output += this.transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey);
1559
+ cursor = node.end;
1560
+ }
1561
+ output += content.slice(cursor);
1562
+ return output;
1563
+ }
1564
+
1565
+ describeExportTarget(exportVar, name) {
1566
+ return /^[A-Za-z_$][\w$]*$/.test(name)
1567
+ ? `${exportVar}.${name}`
1568
+ : `${exportVar}[${JSON.stringify(name)}]`;
1569
+ }
1570
+
1571
+ collectPatternIdentifiers(node, acc = []) {
1572
+ if (!node) return acc;
1573
+ switch (node.type) {
1574
+ case 'Identifier':
1575
+ acc.push(node.name);
1576
+ break;
1577
+ case 'ObjectPattern':
1578
+ for (const prop of node.properties) {
1579
+ if (prop.type === 'RestElement') {
1580
+ this.collectPatternIdentifiers(prop.argument, acc);
1581
+ } else {
1582
+ this.collectPatternIdentifiers(prop.value, acc);
1583
+ }
1584
+ }
1585
+ break;
1586
+ case 'ArrayPattern':
1587
+ for (const element of node.elements) {
1588
+ if (element) this.collectPatternIdentifiers(element, acc);
1589
+ }
1590
+ break;
1591
+ case 'AssignmentPattern':
1592
+ this.collectPatternIdentifiers(node.left, acc);
1593
+ break;
1594
+ case 'RestElement':
1595
+ this.collectPatternIdentifiers(node.argument, acc);
1596
+ break;
1597
+ default:
1598
+ break;
1599
+ }
1600
+ return acc;
1601
+ }
1602
+
1603
+ transformExportedDeclaration(decl, content, exportVar) {
1604
+ const sourceOf = (n) => content.slice(n.start, n.end);
1605
+
1606
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
1607
+ const name = decl.id?.name;
1608
+ if (!name) return sourceOf(decl);
1609
+ // Keep the declaration so other code in the module can still reference the
1610
+ // name (intra-module references), then mirror it onto the exports object.
1611
+ // Each dependency is IIFE-scoped, so this local binding can't collide.
1612
+ return `${sourceOf(decl)}\n${this.describeExportTarget(exportVar, name)} = ${name};`;
1613
+ }
1614
+
1615
+ if (decl.type === 'VariableDeclaration') {
1616
+ // Keep the declaration verbatim — preserving intra-module references and
1617
+ // initializer evaluation — then export every bound name.
1618
+ const names = [];
1619
+ for (const declarator of decl.declarations) {
1620
+ names.push(...this.collectPatternIdentifiers(declarator.id, []));
1621
+ }
1622
+ const assigns = names
1623
+ .map((n) => `${this.describeExportTarget(exportVar, n)} = ${n};`)
1624
+ .join('\n');
1625
+ return `${sourceOf(decl)}\n${assigns}`;
1626
+ }
1627
+
1628
+ return sourceOf(decl);
1629
+ }
1630
+
1631
+ transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey) {
1632
+ const sourceOf = (n) => content.slice(n.start, n.end);
1633
+
1634
+ if (node.type === 'ImportDeclaration') {
1635
+ // Transitive imports of a bundled dependency cannot be resolved at
1636
+ // runtime; strip them so they never leak into the emitted bundle.
1637
+ console.warn(this.buildImportWarningMessage(
1638
+ `Warning: Stripping unsupported import inside bundled dependency: ${node.source?.value}`,
1639
+ moduleName
1640
+ ));
1641
+ return '';
1642
+ }
1643
+
1644
+ if (node.type === 'ExportAllDeclaration') {
1645
+ console.warn(this.buildImportWarningMessage(
1646
+ `Warning: Dropping unsupported 'export *' inside bundled dependency: ${node.source?.value}`,
1647
+ moduleName
1648
+ ));
1649
+ return '';
1650
+ }
1651
+
1652
+ if (node.type === 'ExportDefaultDeclaration') {
1653
+ const declSource = sourceOf(node.declaration);
1654
+ const lines = [`${exportVar}.default = (${declSource});`];
1655
+ if (fallbackKey && fallbackKey !== 'default') {
1656
+ // Preserve the historical `<basename>Data` key so existing default
1657
+ // bindings (which pass it as the preferred key) keep resolving.
1658
+ lines.push(`${this.describeExportTarget(exportVar, fallbackKey)} = ${exportVar}.default;`);
1659
+ }
1660
+ return lines.join('\n');
1661
+ }
1662
+
1663
+ if (node.type === 'ExportNamedDeclaration') {
1664
+ if (node.source) {
1665
+ console.warn(this.buildImportWarningMessage(
1666
+ `Warning: Dropping unsupported re-export inside bundled dependency: ${node.source.value}`,
1667
+ moduleName
1668
+ ));
1669
+ return '';
1670
+ }
1671
+
1672
+ if (node.declaration) {
1673
+ return this.transformExportedDeclaration(node.declaration, content, exportVar);
1674
+ }
1675
+
1676
+ // `export { local as exported, ... }` — key the exports object by the
1677
+ // PUBLIC (exported) name, mapped to the local binding's value.
1678
+ return node.specifiers
1679
+ .map((spec) => {
1680
+ const localName = spec.local.name;
1681
+ const exportedName = spec.exported.name ?? spec.exported.value;
1682
+ return `${this.describeExportTarget(exportVar, exportedName)} = ${localName};`;
1683
+ })
1684
+ .join('\n');
1685
+ }
1686
+
1687
+ // Any other top-level statement is kept verbatim.
1688
+ return sourceOf(node);
1689
+ }
1690
+
1691
+ transformDependencyContentRegexFallback(content, exportVar, moduleName) {
1472
1692
  const dataName = this.getDependencyDefaultFallbackKey(moduleName);
1473
1693
  const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
1474
1694
 
@@ -1594,11 +1814,18 @@ if (window.slice && window.slice.controller) {
1594
1814
  }
1595
1815
 
1596
1816
  toSafeIdentifier(name) {
1597
- const cleaned = name.replace(/[^a-zA-Z0-9_]/g, '_');
1598
- if (/^\d/.test(cleaned)) {
1599
- return `SliceComponent_${cleaned}`;
1600
- }
1601
- return `SliceComponent_${cleaned}`;
1817
+ // Injective encoding: every character outside [A-Za-z0-9] (including '_')
1818
+ // is escaped to `_<hex>_`. This guarantees that two distinct component
1819
+ // names can never collapse to the same identifier (e.g. "my-btn" and
1820
+ // "my_btn" used to both yield "SliceComponent_my_btn", emitting duplicate
1821
+ // `const` declarations and producing invalid bundle JS). The leading
1822
+ // `SliceComponent_` prefix keeps the result a valid identifier even when
1823
+ // the name starts with a digit.
1824
+ const encoded = String(name).replace(
1825
+ /[^a-zA-Z0-9]/g,
1826
+ (char) => `_${char.charCodeAt(0).toString(16)}_`
1827
+ );
1828
+ return `SliceComponent_${encoded}`;
1602
1829
  }
1603
1830
 
1604
1831
  /**
@@ -1633,17 +1860,23 @@ if (window.slice && window.slice.controller) {
1633
1860
  integrity: null,
1634
1861
  components: []
1635
1862
  },
1636
- vendorShared: {
1637
- bundleKey: 'vendor-shared',
1638
- type: 'vendor-shared',
1639
- file: this.vendorShared.file,
1640
- size: this.vendorShared.bundle?.size || 0,
1641
- hash: this.vendorShared.bundle?.hash || null,
1642
- integrity: this.vendorShared.bundle?.integrity || null,
1643
- dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
1644
- dependencyCount: this.vendorShared.sharedDependencySet.size,
1645
- routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
1646
- },
1863
+ // Only advertise the vendor-shared bundle when it was actually emitted
1864
+ // (i.e. there were shared dependencies). Otherwise the config would
1865
+ // reference a file that does not exist on disk -> 404 for any runtime
1866
+ // that resolves it.
1867
+ vendorShared: this.vendorShared.bundle
1868
+ ? {
1869
+ bundleKey: 'vendor-shared',
1870
+ type: 'vendor-shared',
1871
+ file: this.vendorShared.file,
1872
+ size: this.vendorShared.bundle?.size || 0,
1873
+ hash: this.vendorShared.bundle?.hash || null,
1874
+ integrity: this.vendorShared.bundle?.integrity || null,
1875
+ dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
1876
+ dependencyCount: this.vendorShared.sharedDependencySet.size,
1877
+ routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
1878
+ }
1879
+ : null,
1647
1880
  critical: {
1648
1881
  file: this.bundles.critical.file,
1649
1882
  size: this.bundles.critical.size,
@@ -0,0 +1,21 @@
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': 'slice init',
9
+ 'slice:dev': 'slice dev',
10
+ 'slice:build': 'slice build',
11
+ 'slice:start': 'slice start',
12
+ 'slice:create': 'slice component create',
13
+ 'slice:list': 'slice component list',
14
+ 'slice:delete': 'slice component delete',
15
+ 'slice:get': 'slice get',
16
+ 'slice:browse': 'slice browse',
17
+ 'slice:sync': 'slice sync',
18
+ 'slice:version': 'slice version',
19
+ 'slice:update': 'slice update',
20
+ 'slice:types': 'slice types generate',
21
+ };
@@ -7,6 +7,7 @@ import ora from "ora";
7
7
  import Print from "../Print.js";
8
8
  import versionChecker from "./VersionChecker.js";
9
9
  import { getProjectRoot, getApiPath, getPath } from "../utils/PathHelper.js";
10
+ import { resolvePackageManager, installCommand } from "../utils/PackageManager.js";
10
11
  import path from "path";
11
12
  import { fileURLToPath } from "url";
12
13
  import fs from "fs-extra";
@@ -18,28 +19,45 @@ export class UpdateManager {
18
19
  this.packagesToUpdate = [];
19
20
  }
20
21
 
22
+ getPackageManager() {
23
+ if (!this._packageManager) {
24
+ this._packageManager = resolvePackageManager(getProjectRoot(import.meta.url)).name;
25
+ }
26
+ return this._packageManager;
27
+ }
28
+
21
29
  async detectCliInstall() {
22
30
  try {
23
31
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
24
32
  const cliRoot = path.join(moduleDir, '../../');
25
33
  const projectRoot = getProjectRoot(import.meta.url);
34
+ const packageManager = this.getPackageManager();
35
+ const localNodeModules = path.join(projectRoot, 'node_modules');
36
+
37
+ if (cliRoot.startsWith(localNodeModules)) {
38
+ return { type: 'local', cliRoot, projectRoot, packageManager };
39
+ }
40
+
41
+ // Global pnpm installs live under PNPM_HOME — `npm config get prefix`
42
+ // knows nothing about them (and npm may not even exist on the machine).
43
+ const pnpmHome = process.env.PNPM_HOME;
44
+ if (pnpmHome && cliRoot.startsWith(pnpmHome)) {
45
+ return { type: 'global', cliRoot, projectRoot, packageManager: 'pnpm' };
46
+ }
47
+
26
48
  let globalPrefix = '';
27
49
  try {
28
50
  const { stdout } = await execAsync('npm config get prefix');
29
51
  globalPrefix = stdout.toString().trim();
30
52
  } catch {}
31
- const localNodeModules = path.join(projectRoot, 'node_modules');
32
53
  const globalNodeModules = globalPrefix ? path.join(globalPrefix, 'node_modules') : '';
33
-
34
- if (cliRoot.startsWith(localNodeModules)) {
35
- return { type: 'local', cliRoot, projectRoot, globalPrefix };
36
- }
37
54
  if (globalNodeModules && cliRoot.startsWith(globalNodeModules)) {
38
- return { type: 'global', cliRoot, projectRoot, globalPrefix };
55
+ return { type: 'global', cliRoot, projectRoot, packageManager: 'npm' };
39
56
  }
40
- return { type: 'unknown', cliRoot, projectRoot, globalPrefix };
57
+
58
+ return { type: 'unknown', cliRoot, projectRoot, packageManager };
41
59
  } catch (error) {
42
- return { type: 'unknown' };
60
+ return { type: 'unknown', packageManager: this.getPackageManager() };
43
61
  }
44
62
  }
45
63
 
@@ -180,17 +198,12 @@ export class UpdateManager {
180
198
  async buildUpdatePlan(packages) {
181
199
  const plan = [];
182
200
  const info = await this.detectCliInstall();
201
+ const pm = info.packageManager || this.getPackageManager();
183
202
  for (const pkg of packages) {
184
- if (pkg === 'slicejs-cli') {
185
- if (info.type === 'global') {
186
- plan.push({ package: pkg, target: 'global', command: 'npm install -g slicejs-cli@latest' });
187
- } else {
188
- plan.push({ package: pkg, target: 'project', command: 'npm install slicejs-cli@latest' });
189
- }
190
- } else if (pkg === 'slicejs-web-framework') {
191
- plan.push({ package: pkg, target: 'project', command: 'npm install slicejs-web-framework@latest' });
203
+ if (pkg === 'slicejs-cli' && info.type === 'global') {
204
+ plan.push({ package: pkg, target: 'global', command: installCommand(pm, 'slicejs-cli@latest', { global: true }) });
192
205
  } else {
193
- plan.push({ package: pkg, target: 'project', command: `npm install ${pkg}@latest` });
206
+ plan.push({ package: pkg, target: 'project', command: installCommand(pm, `${pkg}@latest`) });
194
207
  }
195
208
  }
196
209
  return plan;
@@ -201,15 +214,15 @@ export class UpdateManager {
201
214
  */
202
215
  async updatePackage(packageName) {
203
216
  try {
204
- let installCmd = `npm install ${packageName}@latest`;
205
- let uninstallCmd = `npm uninstall ${packageName}`;
217
+ const pm = this.getPackageManager();
218
+ let installCmd = installCommand(pm, `${packageName}@latest`);
206
219
  let options = {};
207
220
 
208
221
  if (packageName === 'slicejs-cli') {
209
222
  const info = await this.detectCliInstall();
210
223
  if (info.type === 'global') {
211
- installCmd = `npm install -g slicejs-cli@latest`;
212
- uninstallCmd = `npm uninstall -g slicejs-cli`;
224
+ const globalPm = info.packageManager || pm;
225
+ installCmd = installCommand(globalPm, 'slicejs-cli@latest', { global: true });
213
226
  } else {
214
227
  options.cwd = info.projectRoot || getProjectRoot(import.meta.url);
215
228
  }
@@ -217,11 +230,9 @@ export class UpdateManager {
217
230
  options.cwd = getProjectRoot(import.meta.url);
218
231
  }
219
232
 
220
- // Try uninstall first (ignore failure)
221
- try {
222
- await execAsync(uninstallCmd, options);
223
- } catch {}
224
-
233
+ // Install directly npm/pnpm upgrade in place. (We used to uninstall
234
+ // first, which left the project without the package whenever the
235
+ // subsequent install failed, e.g. offline or under a release-age policy.)
225
236
  const { stdout, stderr } = await execAsync(installCmd, options);
226
237
 
227
238
  return {
@@ -284,13 +295,12 @@ export class UpdateManager {
284
295
  return false;
285
296
  }
286
297
 
287
- if (updateInfo.allCurrent) {
288
- Print.success('✅ All components are up to date!');
289
- return true;
290
- }
291
-
292
- if (!updateInfo.hasUpdates) {
298
+ if (updateInfo.allCurrent || !updateInfo.hasUpdates) {
293
299
  Print.success('✅ All components are up to date!');
300
+ // --update-api works even when no package update runs.
301
+ if (options.updateApi) {
302
+ await this.updateApiIndexIfNeeded(options);
303
+ }
294
304
  return true;
295
305
  }
296
306
 
@@ -341,7 +351,7 @@ export class UpdateManager {
341
351
  }
342
352
  } else {
343
353
  Print.warning('Global CLI detected. It is recommended to update slicejs-cli globally to keep aligned with the framework.');
344
- console.log(' Suggestion: npm install -g slicejs-cli@latest');
354
+ console.log(` Suggestion: ${installCommand(cliInfo.packageManager || this.getPackageManager(), 'slicejs-cli@latest', { global: true })}`);
345
355
  console.log('');
346
356
  }
347
357
  }
@@ -386,7 +396,7 @@ export class UpdateManager {
386
396
  }
387
397
 
388
398
  const frameworkUpdated = results.find(r => r.packageName === 'slicejs-web-framework' && r.success);
389
- if (frameworkUpdated) {
399
+ if (frameworkUpdated || options.updateApi) {
390
400
  await this.updateApiIndexIfNeeded(options);
391
401
  }
392
402
 
@@ -417,7 +427,16 @@ export class UpdateManager {
417
427
 
418
428
  Print.warning('⚠️ Detected changes in framework api/index.js.');
419
429
 
420
- let confirmUpdate = options.yes === true;
430
+ // Overwriting api/index.js is opt-in: `--update-api` is the only way
431
+ // to auto-confirm. `-y/--yes` deliberately does NOT imply it — the
432
+ // project file may carry local changes, so a blanket "yes to updates"
433
+ // must not silently replace it.
434
+ let confirmUpdate = options.updateApi === true;
435
+ if (!confirmUpdate && options.yes === true) {
436
+ Print.info('Skipping api/index.js update (not updated by default).');
437
+ Print.info('Run "slice update --update-api" to update it (a .bak backup is created).');
438
+ return;
439
+ }
421
440
  if (!confirmUpdate) {
422
441
  const answers = await inquirer.prompt([
423
442
  {
package/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "slicejs-cli",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Command client for developing web applications with Slice.js framework",
5
5
  "main": "client.js",
6
6
  "bin": {
7
7
  "slice": "./client.js"
8
8
  },
9
+ "engines": {
10
+ "node": ">=20.0.0"
11
+ },
12
+ "files": [
13
+ "client.js",
14
+ "post.js",
15
+ "commands",
16
+ "assets"
17
+ ],
9
18
  "repository": {
10
19
  "type": "git",
11
20
  "url": "https://github.com/vkneider/slicejs-cli.git"
@@ -14,6 +23,7 @@
14
23
  "test": "node --test",
15
24
  "postinstall": "node post.js",
16
25
  "slice:dev": "slice dev",
26
+ "slice:build": "slice build",
17
27
  "slice:start": "slice start",
18
28
  "slice:create": "slice component create",
19
29
  "slice:list": "slice component list",
@@ -38,6 +48,7 @@
38
48
  ],
39
49
  "author": "vkneider",
40
50
  "type": "module",
51
+ "packageManager": "pnpm@11.1.1",
41
52
  "preferGlobal": false,
42
53
  "license": "ISC",
43
54
  "dependencies": {
@@ -54,5 +65,8 @@
54
65
  "ora": "^8.2.0",
55
66
  "slicejs-web-framework": "latest",
56
67
  "terser": "^5.43.1"
68
+ },
69
+ "devDependencies": {
70
+ "@playwright/test": "^1.60.0"
57
71
  }
58
72
  }
package/post.js CHANGED
@@ -2,35 +2,27 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { getProjectRoot, getPath } from './commands/utils/PathHelper.js';
5
+ import { SLICE_SCRIPTS } from './commands/utils/sliceScripts.js';
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
 
8
- const isGlobal = process.env.npm_config_global === 'true';
9
+ // npm sets npm_config_global; pnpm does not — for pnpm a global install lives
10
+ // under PNPM_HOME, so detect it by where this script is running from.
11
+ const pnpmHome = process.env.PNPM_HOME;
12
+ const isGlobal = process.env.npm_config_global === 'true'
13
+ || (pnpmHome && __filename.startsWith(pnpmHome));
9
14
 
10
15
  if (isGlobal) {
11
16
  console.log('⚠️ Global installation of slicejs-cli detected.');
12
17
  console.log(' We strongly recommend using a local installation to avoid version mismatches.');
13
- console.log(' Uninstall global: npm uninstall -g slicejs-cli');
18
+ console.log(` Uninstall global: ${pnpmHome ? 'pnpm remove -g slicejs-cli' : 'npm uninstall -g slicejs-cli'}`);
14
19
  process.exit(0);
15
20
  }
16
21
 
17
22
  const projectRoot = getProjectRoot(import.meta.url);
18
23
  const pkgPath = getPath(import.meta.url, 'package.json');
19
24
 
20
- const sliceScripts = {
21
- 'slice:dev': 'slice dev',
22
- 'slice:start': 'slice start',
23
- 'slice:create': 'slice component create',
24
- 'slice:list': 'slice component list',
25
- 'slice:delete': 'slice component delete',
26
- 'slice:init': 'slice init',
27
- 'slice:get': 'slice get',
28
- 'slice:browse': 'slice browse',
29
- 'slice:sync': 'slice sync',
30
- 'slice:version': 'slice version',
31
- 'slice:update': 'slice update',
32
- 'slice:types': 'slice types generate',
33
- };
25
+ const sliceScripts = SLICE_SCRIPTS;
34
26
 
35
27
  try {
36
28
  let pkg = {};