@jskit-ai/jskit-cli 0.2.73 → 0.2.75

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.73",
3
+ "version": "0.2.75",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,9 +20,9 @@
20
20
  "test": "node --test"
21
21
  },
22
22
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.72",
24
- "@jskit-ai/kernel": "0.1.64",
25
- "@jskit-ai/shell-web": "0.1.63"
23
+ "@jskit-ai/jskit-catalog": "0.1.74",
24
+ "@jskit-ai/kernel": "0.1.66",
25
+ "@jskit-ai/shell-web": "0.1.65"
26
26
  },
27
27
  "engines": {
28
28
  "node": "20.x"
@@ -2,6 +2,10 @@ import path from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { access, readdir, readFile } from "node:fs/promises";
4
4
  import { buildCrudFieldContractMap } from "@jskit-ai/kernel/shared/support/crudFieldContract";
5
+ import {
6
+ discoverPlacementTopologyFromApp,
7
+ discoverShellOutletTargetsFromApp
8
+ } from "@jskit-ai/kernel/server/support";
5
9
  import {
6
10
  buildAppCommandOptionMeta,
7
11
  listAppCommandDefinitions
@@ -370,6 +374,37 @@ async function discoverPlacementTargets(appRoot) {
370
374
  return uniqueSorted(placementValues);
371
375
  }
372
376
 
377
+ async function discoverSemanticPlacementTargets(appRoot) {
378
+ try {
379
+ const topology = await discoverPlacementTopologyFromApp({ appRoot });
380
+ return uniqueSorted(
381
+ (Array.isArray(topology?.placements) ? topology.placements : [])
382
+ .map((placement) => normalizeText(placement?.id))
383
+ .filter(Boolean)
384
+ );
385
+ } catch {
386
+ const topologyPath = path.join(appRoot, "src", "placementTopology.js");
387
+ if (!(await pathExists(topologyPath))) {
388
+ return [];
389
+ }
390
+ const source = await readFile(topologyPath, "utf8");
391
+ return uniqueSorted(extractMatches(source, [/\bid\s*:\s*["']([^"':]+(?:\.[^"':]+)+)["']/g]));
392
+ }
393
+ }
394
+
395
+ async function discoverConcretePlacementTargets(appRoot) {
396
+ try {
397
+ const discovered = await discoverShellOutletTargetsFromApp({ appRoot });
398
+ return uniqueSorted(
399
+ (Array.isArray(discovered?.targets) ? discovered.targets : [])
400
+ .map((target) => normalizeText(target?.id || target?.target))
401
+ .filter(Boolean)
402
+ );
403
+ } catch {
404
+ return discoverPlacementTargets(appRoot);
405
+ }
406
+ }
407
+
373
408
  async function discoverComponentTokens(appRoot) {
374
409
  const tokens = [];
375
410
  for (const filePath of [
@@ -391,7 +426,6 @@ async function discoverComponentTokens(appRoot) {
391
426
  const source = await readFile(filePath, "utf8");
392
427
  tokens.push(...extractMatches(source, [
393
428
  /\bcomponentToken\s*:\s*["']([^"']+)["']/g,
394
- /\bdefault-link-component-token\s*=\s*["']([^"']+)["']/g,
395
429
  /registerMainClientComponent\(\s*["']([^"']+)["']/g
396
430
  ]));
397
431
  }
@@ -590,9 +624,11 @@ async function completeOptionValue({
590
624
 
591
625
  if (normalizedOptionName === "resource-file") {
592
626
  suggestions = await discoverResourceFiles(appRoot);
593
- } else if (["link-placement", "placement", "target"].includes(normalizedOptionName)) {
594
- suggestions = await discoverPlacementTargets(appRoot);
595
- } else if (normalizedOptionName === "link-component-token") {
627
+ } else if (["link-placement", "placement"].includes(normalizedOptionName)) {
628
+ suggestions = await discoverSemanticPlacementTargets(appRoot);
629
+ } else if (normalizedOptionName === "target") {
630
+ suggestions = await discoverConcretePlacementTargets(appRoot);
631
+ } else if (normalizedOptionName === "link-renderer") {
596
632
  suggestions = await discoverComponentTokens(appRoot);
597
633
  } else if (normalizedOptionName === "surface") {
598
634
  suggestions = await discoverSurfaces(appRoot);
@@ -3,7 +3,11 @@ import {
3
3
  ensureObject
4
4
  } from "../../shared/collectionUtils.js";
5
5
  import {
6
- normalizeShellOutletTargetId
6
+ normalizePlacementKind,
7
+ normalizePlacementOwnerId,
8
+ normalizePlacementTopologyDefinition,
9
+ normalizeShellOutletTargetId,
10
+ resolvePlacementTargetReference
7
11
  } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
8
12
 
9
13
  function normalizePlacementOutlets(value) {
@@ -41,22 +45,27 @@ function normalizePlacementContributions(value) {
41
45
  for (const entry of ensureArray(value)) {
42
46
  const record = ensureObject(entry);
43
47
  const id = String(record.id || "").trim();
44
- const target = normalizeShellOutletTargetId(record.target);
45
- if (!id || !target) {
48
+ const targetReference = resolvePlacementTargetReference(record.target);
49
+ if (!id || !targetReference?.id) {
46
50
  continue;
47
51
  }
48
52
 
49
53
  const surfaces = [...new Set(ensureArray(record.surfaces).map((item) => String(item || "").trim()).filter(Boolean))];
54
+ const kind = normalizePlacementKind(record.kind) || (String(record.componentToken || "").trim() ? "component" : "link");
50
55
  const componentToken = String(record.componentToken || "").trim();
51
56
  const when = String(record.when || "").trim();
52
57
  const description = String(record.description || "").trim();
53
58
  const source = String(record.source || "").trim();
59
+ const owner = normalizePlacementOwnerId(record.owner);
54
60
  const parsedOrder = Number(record.order);
55
61
  const order = Number.isFinite(parsedOrder) ? Math.trunc(parsedOrder) : null;
56
62
  contributions.push(
57
63
  Object.freeze({
58
64
  id,
59
- target,
65
+ target: targetReference.id,
66
+ targetType: targetReference.type,
67
+ owner,
68
+ kind,
60
69
  surfaces: Object.freeze(surfaces),
61
70
  order,
62
71
  componentToken,
@@ -83,7 +92,12 @@ function normalizePlacementContributions(value) {
83
92
  );
84
93
  }
85
94
 
95
+ function normalizePlacementTopology(value, { context = "package placement topology" } = {}) {
96
+ return normalizePlacementTopologyDefinition(value, { context }).placements;
97
+ }
98
+
86
99
  export {
87
100
  normalizePlacementContributions,
88
- normalizePlacementOutlets
101
+ normalizePlacementOutlets,
102
+ normalizePlacementTopology
89
103
  };
@@ -177,6 +177,7 @@ function createListCommands(ctx = {}) {
177
177
  loadBundleRegistry,
178
178
  loadAppLocalPackageRegistry,
179
179
  resolveInstalledNodeModulePackageEntry,
180
+ discoverPlacementTopologyFromApp,
180
181
  discoverShellOutletTargetsFromApp,
181
182
  normalizePlacementContributions,
182
183
  resolvePackageKind
@@ -435,39 +436,99 @@ function createListCommands(ctx = {}) {
435
436
 
436
437
  async function commandListPlacements({ options, cwd, stdout }) {
437
438
  const appRoot = await resolveAppRootFromCwd(cwd);
438
- const discoveredPlacements = await discoverShellOutletTargetsFromApp({
439
- appRoot,
440
- sourceRoot: "src"
441
- });
442
- const placementTargets = ensureArray(discoveredPlacements.targets)
439
+ const showConcreteOnly = options.concrete === true && options.all !== true;
440
+ const showConcrete = options.concrete === true || options.all === true;
441
+ const showSemantic = showConcreteOnly !== true;
442
+ const discoveredTopology = await discoverPlacementTopologyFromApp({ appRoot });
443
+ const semanticPlacements = ensureArray(discoveredTopology.placements)
444
+ .map((entry) => ensureObject(entry))
445
+ .filter((entry) => String(entry.id || "").trim())
446
+ .sort((left, right) => {
447
+ const idCompare = String(left.id || "").localeCompare(String(right.id || ""));
448
+ if (idCompare !== 0) {
449
+ return idCompare;
450
+ }
451
+ return String(left.owner || "").localeCompare(String(right.owner || ""));
452
+ });
453
+ const discoveredConcrete = showConcrete
454
+ ? await discoverShellOutletTargetsFromApp({
455
+ appRoot,
456
+ sourceRoot: "src"
457
+ })
458
+ : { targets: [] };
459
+ const concreteTargets = ensureArray(discoveredConcrete.targets)
443
460
  .map((entry) => ensureObject(entry))
444
461
  .filter((entry) => String(entry.id || "").trim())
445
462
  .sort((left, right) => String(left.id || "").localeCompare(String(right.id || "")));
446
463
 
447
464
  if (options.json) {
448
465
  const payload = {
449
- placements: placementTargets.map((placementTarget) => ({
450
- target: String(placementTarget.id || "").trim(),
451
- default: placementTarget.default === true,
452
- sourcePath: String(placementTarget.sourcePath || "").trim()
453
- }))
466
+ placements: showSemantic
467
+ ? semanticPlacements.map((placementTarget) => ({
468
+ target: String(placementTarget.id || "").trim(),
469
+ owner: String(placementTarget.owner || "").trim(),
470
+ default: placementTarget.default === true,
471
+ description: String(placementTarget.description || "").trim(),
472
+ surfaces: ensureArray(placementTarget.surfaces).map((entry) => String(entry || "").trim()).filter(Boolean),
473
+ variants: ensureObject(placementTarget.variants),
474
+ sourcePath: String(placementTarget.sourcePath || "").trim()
475
+ }))
476
+ : [],
477
+ concretePlacements: showConcrete
478
+ ? concreteTargets.map((placementTarget) => ({
479
+ target: String(placementTarget.id || "").trim(),
480
+ default: placementTarget.default === true,
481
+ sourcePath: String(placementTarget.sourcePath || "").trim()
482
+ }))
483
+ : []
454
484
  };
455
485
  stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
456
486
  return 0;
457
487
  }
458
488
 
459
489
  const color = createColorFormatter(stdout);
460
- const lines = [color.heading("Available placements:")];
461
- if (placementTargets.length < 1) {
462
- lines.push("- none");
463
- } else {
464
- for (const placementTarget of placementTargets) {
465
- const placementId = String(placementTarget.id || "").trim();
466
- const sourcePath = String(placementTarget.sourcePath || "").trim();
467
- const isDefault = placementTarget.default === true;
468
- const defaultLabel = isDefault ? color.installed(" (default)") : "";
469
- const sourceLabel = sourcePath ? ` ${color.dim(`[${sourcePath}]`)}` : "";
470
- lines.push(`- ${color.item(placementId)}${defaultLabel}${sourceLabel}`);
490
+ const lines = [];
491
+ if (showSemantic) {
492
+ lines.push(color.heading("Available placements:"));
493
+ if (semanticPlacements.length < 1) {
494
+ lines.push("- none");
495
+ } else {
496
+ for (const placementTarget of semanticPlacements) {
497
+ const placementId = String(placementTarget.id || "").trim();
498
+ const owner = String(placementTarget.owner || "").trim();
499
+ const ownerLabel = owner ? color.dim(` [owner:${owner}]`) : "";
500
+ const defaultLabel = placementTarget.default === true ? color.installed(" (default)") : "";
501
+ const description = String(placementTarget.description || "").trim();
502
+ const descriptionSuffix = description ? `: ${description}` : "";
503
+ lines.push(`- ${color.item(placementId)}${ownerLabel}${defaultLabel}${descriptionSuffix}`);
504
+ const variants = ensureObject(placementTarget.variants);
505
+ for (const layoutClass of ["compact", "medium", "expanded"]) {
506
+ const variant = ensureObject(variants[layoutClass]);
507
+ const outlet = String(variant.outlet || "").trim();
508
+ if (outlet) {
509
+ lines.push(` ${layoutClass} -> ${color.dim(outlet)}`);
510
+ }
511
+ }
512
+ }
513
+ }
514
+ }
515
+
516
+ if (showConcrete) {
517
+ if (lines.length > 0) {
518
+ lines.push("");
519
+ }
520
+ lines.push(color.heading("Available concrete outlets:"));
521
+ if (concreteTargets.length < 1) {
522
+ lines.push("- none");
523
+ } else {
524
+ for (const placementTarget of concreteTargets) {
525
+ const placementId = String(placementTarget.id || "").trim();
526
+ const sourcePath = String(placementTarget.sourcePath || "").trim();
527
+ const isDefault = placementTarget.default === true;
528
+ const defaultLabel = isDefault ? color.installed(" (default)") : "";
529
+ const sourceLabel = sourcePath ? ` ${color.dim(`[${sourcePath}]`)}` : "";
530
+ lines.push(`- ${color.item(placementId)}${defaultLabel}${sourceLabel}`);
531
+ }
471
532
  }
472
533
  }
473
534
 
@@ -60,7 +60,7 @@ async function collectManagedMobileFileDriftIssues({
60
60
  }
61
61
  if (currentContent !== expectedContent) {
62
62
  issues.push(
63
- `${normalizeRelativePath(appRoot, absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile sync android to refresh managed mobile-shell files.`
63
+ `${normalizeRelativePath(appRoot, absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile android sync to refresh managed mobile-shell files.`
64
64
  );
65
65
  }
66
66
  }
@@ -106,24 +106,53 @@ async function collectMissingInstalledDependencyNames(ctx, appRoot = "", package
106
106
  return missing;
107
107
  }
108
108
 
109
- function renderMobileHelp(stream, definition = null) {
109
+ function renderAndroidMobileCommandList(lines, color) {
110
+ for (const entry of listMobileCommandDefinitions()) {
111
+ if (entry.name === "dev") {
112
+ lines.push(
113
+ ` - ${color.item(entry.name)}: Shortcut to run ${color.emphasis("sync")}, ${color.emphasis("tunnel")}, ${color.emphasis("run")} in this order`
114
+ );
115
+ continue;
116
+ }
117
+ lines.push(` - ${color.item(entry.name)}: ${entry.summary}`);
118
+ }
119
+ }
120
+
121
+ function renderMobileHelp(stream, definition = null, platform = "") {
110
122
  const color = createColorFormatter(stream);
111
123
  const lines = [];
112
124
 
113
- if (!definition) {
125
+ if (!definition && !platform) {
114
126
  lines.push(`Command: ${color.emphasis("mobile")}`);
115
127
  lines.push("");
116
128
  lines.push(color.heading("1) Minimal use"));
117
- lines.push(" jskit mobile <subcommand>");
129
+ lines.push(" jskit mobile <platform> <subcommand>");
118
130
  lines.push("");
119
- lines.push(color.heading("2) Subcommands"));
120
- for (const entry of listMobileCommandDefinitions()) {
121
- lines.push(` - ${color.item(entry.name)}: ${entry.summary}`);
122
- }
131
+ lines.push(color.heading("2) Platforms"));
132
+ lines.push(` - ${color.item("android")}`);
133
+ renderAndroidMobileCommandList(lines, color);
123
134
  lines.push("");
124
135
  lines.push(color.heading("3) Notes"));
125
136
  lines.push(" - Mobile helpers are for the Stage 1 Android Capacitor shell flow.");
126
- lines.push(" - Use jskit mobile <subcommand> help for subcommand-specific usage.");
137
+ lines.push(" - Use jskit mobile <platform> help for platform-specific usage.");
138
+ writeWrappedLines({
139
+ stdout: stream,
140
+ lines
141
+ });
142
+ return;
143
+ }
144
+
145
+ if (!definition) {
146
+ lines.push(`Mobile platform: ${color.emphasis(platform)}`);
147
+ lines.push("");
148
+ lines.push(color.heading("1) Minimal use"));
149
+ lines.push(` jskit mobile ${platform} <subcommand>`);
150
+ lines.push("");
151
+ lines.push(color.heading("2) Subcommands"));
152
+ renderAndroidMobileCommandList(lines, color);
153
+ lines.push("");
154
+ lines.push(color.heading("3) Notes"));
155
+ lines.push(` - Use jskit mobile ${platform} <subcommand> help for subcommand-specific usage.`);
127
156
  writeWrappedLines({
128
157
  stdout: stream,
129
158
  lines
@@ -256,7 +285,7 @@ function resolveAdbReversePort({
256
285
  const port = Number(parsedUrl.port || "");
257
286
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
258
287
  throw createCliError(
259
- `config.mobile.apiBaseUrl "${apiBaseUrl}" must include an explicit port so jskit mobile tunnel android can infer adb reverse.`
288
+ `config.mobile.apiBaseUrl "${apiBaseUrl}" must include an explicit port so jskit mobile android tunnel can infer adb reverse.`
260
289
  );
261
290
  }
262
291
 
@@ -337,7 +366,7 @@ async function resolveAndroidDeviceTarget({
337
366
  appRoot
338
367
  });
339
368
  if (devices.length < 1) {
340
- throw ctx.createCliError(`No Android devices are visible to adb. Run jskit mobile devices android before ${commandLabel}.`);
369
+ throw ctx.createCliError(`No Android devices are visible to adb. Run jskit mobile android devices before ${commandLabel}.`);
341
370
  }
342
371
 
343
372
  const normalizedExplicitTarget = String(explicitTarget || "").trim();
@@ -346,7 +375,7 @@ async function resolveAndroidDeviceTarget({
346
375
  : devices[0];
347
376
 
348
377
  if (!selectedDevice) {
349
- throw ctx.createCliError(`Android device "${normalizedExplicitTarget}" is not visible to adb. Run jskit mobile devices android first.`);
378
+ throw ctx.createCliError(`Android device "${normalizedExplicitTarget}" is not visible to adb. Run jskit mobile android devices first.`);
350
379
  }
351
380
  if (selectedDevice.state !== "device") {
352
381
  throw ctx.createCliError(`Android device "${selectedDevice.serial}" is currently "${selectedDevice.state}", not ready for ${commandLabel}.`);
@@ -402,7 +431,7 @@ async function runLocalBinary(binaryName, args = [], {
402
431
  if (error?.code === "ENOENT") {
403
432
  reject(
404
433
  createCliError(
405
- `Could not find local "${binaryName}" in node_modules/.bin. Re-run jskit mobile add capacitor after npm install succeeds.`
434
+ `Could not find local "${binaryName}" in node_modules/.bin. Re-run jskit add package @jskit-ai/mobile-capacitor after npm install succeeds.`
406
435
  )
407
436
  );
408
437
  return;
@@ -513,28 +542,6 @@ async function refreshManagedMobileFiles({
513
542
  }
514
543
  }
515
544
 
516
- async function runMobileAddCapacitorCommand({
517
- commandAdd,
518
- appRoot,
519
- options = {},
520
- stdout,
521
- stderr
522
- }) {
523
- return await commandAdd({
524
- positional: ["package", CAPACITOR_RUNTIME_PACKAGE_ID],
525
- options: {
526
- ...options,
527
- runNpmInstall: true,
528
- inlineOptions: {}
529
- },
530
- cwd: appRoot,
531
- io: {
532
- stdout,
533
- stderr
534
- }
535
- });
536
- }
537
-
538
545
  async function runMobileSyncAndroidCommand({
539
546
  ctx,
540
547
  commandAdd,
@@ -738,7 +745,7 @@ async function runMobileBuildAndroidCommand({
738
745
 
739
746
  if (mobileConfig.assetMode !== "bundled") {
740
747
  throw createCliError(
741
- 'jskit mobile build android requires config.mobile.assetMode="bundled" so the release shell does not depend on a live dev server.'
748
+ 'jskit mobile android build requires config.mobile.assetMode="bundled" so the release shell does not depend on a live dev server.'
742
749
  );
743
750
  }
744
751
 
@@ -906,9 +913,6 @@ async function runMobileTunnelAndroidCommand({
906
913
  }) {
907
914
  const inlineOptions = normalizeInlineOptions(options);
908
915
  const target = String(inlineOptions.target || "").trim();
909
- if (!target) {
910
- throw ctx.createCliError("jskit mobile tunnel android requires --target <device-id>.");
911
- }
912
916
 
913
917
  const mobileConfig = await resolveInstalledMobileConfigForCommand({
914
918
  appRoot,
@@ -960,9 +964,6 @@ async function runMobileRestartAndroidCommand({
960
964
  }) {
961
965
  const inlineOptions = normalizeInlineOptions(options);
962
966
  const target = String(inlineOptions.target || "").trim();
963
- if (!target) {
964
- throw ctx.createCliError("jskit mobile restart android requires --target <device-id>.");
965
- }
966
967
 
967
968
  const mobileConfig = await resolveInstalledMobileConfigForCommand({
968
969
  appRoot,
@@ -1020,7 +1021,7 @@ async function runMobileDevAndroidCommand({
1020
1021
 
1021
1022
  stdout.write(`[mobile] Using Android device: ${selectedDevice.serial}\n`);
1022
1023
  stdout.write("[mobile] Building and syncing the Android shell:\n");
1023
- stdout.write("[mobile] npx jskit mobile sync android\n");
1024
+ stdout.write("[mobile] npx jskit mobile android sync\n");
1024
1025
  await runMobileSyncAndroidCommand({
1025
1026
  ctx,
1026
1027
  commandAdd,
@@ -1030,20 +1031,8 @@ async function runMobileDevAndroidCommand({
1030
1031
  stderr
1031
1032
  });
1032
1033
 
1033
- stdout.write(`[mobile] Installing and launching the app on ${selectedDevice.serial}:\n`);
1034
- stdout.write(`[mobile] npx jskit mobile run android --target ${selectedDevice.serial}\n`);
1035
- await runCapRunAndroidCommand({
1036
- ctx,
1037
- appRoot,
1038
- pathModule: ctx.path,
1039
- target: selectedDevice.serial,
1040
- stdout,
1041
- stderr,
1042
- dryRun: false
1043
- });
1044
-
1045
1034
  stdout.write(`[mobile] Creating the adb reverse tunnel on ${selectedDevice.serial}:\n`);
1046
- stdout.write(`[mobile] npx jskit mobile tunnel android --target ${selectedDevice.serial}\n`);
1035
+ stdout.write(`[mobile] npx jskit mobile android tunnel --target ${selectedDevice.serial}\n`);
1047
1036
  await runMobileTunnelAndroidCommand({
1048
1037
  ctx,
1049
1038
  appRoot,
@@ -1056,6 +1045,18 @@ async function runMobileDevAndroidCommand({
1056
1045
  stderr
1057
1046
  });
1058
1047
 
1048
+ stdout.write(`[mobile] Installing and launching the app on ${selectedDevice.serial}:\n`);
1049
+ stdout.write(`[mobile] npx jskit mobile android run --target ${selectedDevice.serial}\n`);
1050
+ await runCapRunAndroidCommand({
1051
+ ctx,
1052
+ appRoot,
1053
+ pathModule: ctx.path,
1054
+ target: selectedDevice.serial,
1055
+ stdout,
1056
+ stderr,
1057
+ dryRun: false
1058
+ });
1059
+
1059
1060
  return 0;
1060
1061
  }
1061
1062
 
@@ -1072,7 +1073,8 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1072
1073
  async function commandMobile({ positional = [], options = {}, cwd = "", stdout, stderr }) {
1073
1074
  const firstToken = String(positional[0] || "").trim();
1074
1075
  const secondToken = String(positional[1] || "").trim();
1075
- const remainingPositionals = positional.slice(2);
1076
+ const thirdToken = String(positional[2] || "").trim();
1077
+ const remainingPositionals = positional.slice(3);
1076
1078
 
1077
1079
  if (!firstToken) {
1078
1080
  renderMobileHelp(stdout);
@@ -1080,19 +1082,31 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1080
1082
  }
1081
1083
 
1082
1084
  if (firstToken === "help") {
1083
- renderMobileHelp(stdout, resolveMobileCommandDefinition(secondToken));
1085
+ renderMobileHelp(stdout);
1084
1086
  return 0;
1085
1087
  }
1086
1088
 
1087
- const definition = resolveMobileCommandDefinition(firstToken);
1088
- if (!definition) {
1089
- throw createCliError(`Unknown mobile subcommand: ${firstToken}.`, {
1089
+ const platform = firstToken;
1090
+ if (platform !== "android") {
1091
+ throw createCliError(`Unknown mobile platform: ${platform}.`, {
1090
1092
  renderUsage: () => renderMobileHelp(stderr)
1091
1093
  });
1092
1094
  }
1093
1095
 
1094
- if (secondToken === "help") {
1095
- renderMobileHelp(stdout, definition);
1096
+ if (!secondToken || secondToken === "help") {
1097
+ renderMobileHelp(stdout, null, platform);
1098
+ return 0;
1099
+ }
1100
+
1101
+ const definition = resolveMobileCommandDefinition(secondToken);
1102
+ if (!definition) {
1103
+ throw createCliError(`Unknown mobile ${platform} subcommand: ${secondToken}.`, {
1104
+ renderUsage: () => renderMobileHelp(stderr, null, platform)
1105
+ });
1106
+ }
1107
+
1108
+ if (thirdToken === "help") {
1109
+ renderMobileHelp(stdout, definition, platform);
1096
1110
  return 0;
1097
1111
  }
1098
1112
 
@@ -1102,14 +1116,14 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1102
1116
  const unknownInlineOptionNames = inlineOptionNames.filter((optionName) => !supportedOptionNames.has(optionName));
1103
1117
  if (unknownInlineOptionNames.length > 0) {
1104
1118
  throw createCliError(
1105
- `Unknown option${unknownInlineOptionNames.length === 1 ? "" : "s"} for jskit mobile ${definition.name}: ${unknownInlineOptionNames.map((optionName) => `--${optionName}`).join(", ")}.`,
1119
+ `Unknown option${unknownInlineOptionNames.length === 1 ? "" : "s"} for jskit mobile ${platform} ${definition.name}: ${unknownInlineOptionNames.map((optionName) => `--${optionName}`).join(", ")}.`,
1106
1120
  {
1107
1121
  renderUsage: () => renderMobileHelp(stderr, definition)
1108
1122
  }
1109
1123
  );
1110
1124
  }
1111
1125
  if (options?.dryRun === true && !supportedOptionNames.has("dry-run")) {
1112
- throw createCliError(`Unknown option for jskit mobile ${definition.name}: --dry-run.`, {
1126
+ throw createCliError(`Unknown option for jskit mobile ${platform} ${definition.name}: --dry-run.`, {
1113
1127
  renderUsage: () => renderMobileHelp(stderr, definition)
1114
1128
  });
1115
1129
  }
@@ -1117,13 +1131,8 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1117
1131
  const appRoot = await resolveAppRootFromCwd(cwd);
1118
1132
 
1119
1133
  if (definition.name === "devices") {
1120
- if (secondToken !== "android") {
1121
- throw createCliError(`jskit mobile devices currently supports only "android".`, {
1122
- renderUsage: () => renderMobileHelp(stderr, definition)
1123
- });
1124
- }
1125
- if (remainingPositionals.length > 0) {
1126
- throw createCliError(`Unexpected positional arguments for jskit mobile devices: ${remainingPositionals.join(" ")}`, {
1134
+ if (thirdToken || remainingPositionals.length > 0) {
1135
+ throw createCliError(`Unexpected positional arguments for jskit mobile ${platform} devices: ${[thirdToken, ...remainingPositionals].filter(Boolean).join(" ")}`, {
1127
1136
  renderUsage: () => renderMobileHelp(stderr, definition)
1128
1137
  });
1129
1138
  }
@@ -1137,13 +1146,8 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1137
1146
  }
1138
1147
 
1139
1148
  if (definition.name === "dev") {
1140
- if (secondToken !== "android") {
1141
- throw createCliError(`jskit mobile dev currently supports only "android".`, {
1142
- renderUsage: () => renderMobileHelp(stderr, definition)
1143
- });
1144
- }
1145
- if (remainingPositionals.length > 0) {
1146
- throw createCliError(`Unexpected positional arguments for jskit mobile dev: ${remainingPositionals.join(" ")}`, {
1149
+ if (thirdToken || remainingPositionals.length > 0) {
1150
+ throw createCliError(`Unexpected positional arguments for jskit mobile ${platform} dev: ${[thirdToken, ...remainingPositionals].filter(Boolean).join(" ")}`, {
1147
1151
  renderUsage: () => renderMobileHelp(stderr, definition)
1148
1152
  });
1149
1153
  }
@@ -1159,13 +1163,8 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1159
1163
  }
1160
1164
 
1161
1165
  if (definition.name === "tunnel") {
1162
- if (secondToken !== "android") {
1163
- throw createCliError(`jskit mobile tunnel currently supports only "android".`, {
1164
- renderUsage: () => renderMobileHelp(stderr, definition)
1165
- });
1166
- }
1167
- if (remainingPositionals.length > 0) {
1168
- throw createCliError(`Unexpected positional arguments for jskit mobile tunnel: ${remainingPositionals.join(" ")}`, {
1166
+ if (thirdToken || remainingPositionals.length > 0) {
1167
+ throw createCliError(`Unexpected positional arguments for jskit mobile ${platform} tunnel: ${[thirdToken, ...remainingPositionals].filter(Boolean).join(" ")}`, {
1169
1168
  renderUsage: () => renderMobileHelp(stderr, definition)
1170
1169
  });
1171
1170
  }
@@ -1180,13 +1179,8 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1180
1179
  }
1181
1180
 
1182
1181
  if (definition.name === "restart") {
1183
- if (secondToken !== "android") {
1184
- throw createCliError(`jskit mobile restart currently supports only "android".`, {
1185
- renderUsage: () => renderMobileHelp(stderr, definition)
1186
- });
1187
- }
1188
- if (remainingPositionals.length > 0) {
1189
- throw createCliError(`Unexpected positional arguments for jskit mobile restart: ${remainingPositionals.join(" ")}`, {
1182
+ if (thirdToken || remainingPositionals.length > 0) {
1183
+ throw createCliError(`Unexpected positional arguments for jskit mobile ${platform} restart: ${[thirdToken, ...remainingPositionals].filter(Boolean).join(" ")}`, {
1190
1184
  renderUsage: () => renderMobileHelp(stderr, definition)
1191
1185
  });
1192
1186
  }
@@ -1200,36 +1194,9 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1200
1194
  });
1201
1195
  }
1202
1196
 
1203
- if (definition.name === "add") {
1204
- if (secondToken !== "capacitor") {
1205
- throw createCliError(`jskit mobile add currently supports only "capacitor".`, {
1206
- renderUsage: () => renderMobileHelp(stderr, definition)
1207
- });
1208
- }
1209
- if (remainingPositionals.length > 0) {
1210
- throw createCliError(`Unexpected positional arguments for jskit mobile add: ${remainingPositionals.join(" ")}`, {
1211
- renderUsage: () => renderMobileHelp(stderr, definition)
1212
- });
1213
- }
1214
-
1215
- return runMobileAddCapacitorCommand({
1216
- ctx,
1217
- commandAdd,
1218
- appRoot,
1219
- options,
1220
- stdout,
1221
- stderr
1222
- });
1223
- }
1224
-
1225
1197
  if (definition.name === "sync") {
1226
- if (secondToken !== "android") {
1227
- throw createCliError(`jskit mobile sync currently supports only "android".`, {
1228
- renderUsage: () => renderMobileHelp(stderr, definition)
1229
- });
1230
- }
1231
- if (remainingPositionals.length > 0) {
1232
- throw createCliError(`Unexpected positional arguments for jskit mobile sync: ${remainingPositionals.join(" ")}`, {
1198
+ if (thirdToken || remainingPositionals.length > 0) {
1199
+ throw createCliError(`Unexpected positional arguments for jskit mobile ${platform} sync: ${[thirdToken, ...remainingPositionals].filter(Boolean).join(" ")}`, {
1233
1200
  renderUsage: () => renderMobileHelp(stderr, definition)
1234
1201
  });
1235
1202
  }
@@ -1245,13 +1212,8 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1245
1212
  }
1246
1213
 
1247
1214
  if (definition.name === "run") {
1248
- if (secondToken !== "android") {
1249
- throw createCliError(`jskit mobile run currently supports only "android".`, {
1250
- renderUsage: () => renderMobileHelp(stderr, definition)
1251
- });
1252
- }
1253
- if (remainingPositionals.length > 0) {
1254
- throw createCliError(`Unexpected positional arguments for jskit mobile run: ${remainingPositionals.join(" ")}`, {
1215
+ if (thirdToken || remainingPositionals.length > 0) {
1216
+ throw createCliError(`Unexpected positional arguments for jskit mobile ${platform} run: ${[thirdToken, ...remainingPositionals].filter(Boolean).join(" ")}`, {
1255
1217
  renderUsage: () => renderMobileHelp(stderr, definition)
1256
1218
  });
1257
1219
  }
@@ -1267,13 +1229,8 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1267
1229
  }
1268
1230
 
1269
1231
  if (definition.name === "build") {
1270
- if (secondToken !== "android") {
1271
- throw createCliError(`jskit mobile build currently supports only "android".`, {
1272
- renderUsage: () => renderMobileHelp(stderr, definition)
1273
- });
1274
- }
1275
- if (remainingPositionals.length > 0) {
1276
- throw createCliError(`Unexpected positional arguments for jskit mobile build: ${remainingPositionals.join(" ")}`, {
1232
+ if (thirdToken || remainingPositionals.length > 0) {
1233
+ throw createCliError(`Unexpected positional arguments for jskit mobile ${platform} build: ${[thirdToken, ...remainingPositionals].filter(Boolean).join(" ")}`, {
1277
1234
  renderUsage: () => renderMobileHelp(stderr, definition)
1278
1235
  });
1279
1236
  }
@@ -1289,8 +1246,8 @@ function createMobileCommands(ctx = {}, { commandAdd } = {}) {
1289
1246
  }
1290
1247
 
1291
1248
  if (definition.name === "doctor") {
1292
- if (secondToken) {
1293
- throw createCliError(`Unexpected positional arguments for jskit mobile doctor: ${[secondToken, ...remainingPositionals].join(" ")}`, {
1249
+ if (thirdToken || remainingPositionals.length > 0) {
1250
+ throw createCliError(`Unexpected positional arguments for jskit mobile ${platform} doctor: ${[thirdToken, ...remainingPositionals].filter(Boolean).join(" ")}`, {
1294
1251
  renderUsage: () => renderMobileHelp(stderr, definition)
1295
1252
  });
1296
1253
  }
@@ -1,30 +1,8 @@
1
1
  const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
2
- add: Object.freeze({
3
- name: "add",
4
- summary: "Install Capacitor mobile-shell support into the current app.",
5
- usage: "jskit mobile add capacitor [--dry-run] [--devlinks]",
6
- options: Object.freeze([
7
- Object.freeze({
8
- label: "--devlinks",
9
- description: "Run npm run --if-present devlinks after install for local development relinking."
10
- }),
11
- Object.freeze({
12
- label: "--dry-run",
13
- description: "Preview the package install and generated files without mutating package.json, lockfiles, or app files."
14
- })
15
- ]),
16
- defaults: Object.freeze([
17
- "Installs @jskit-ai/mobile-capacitor plus the required Capacitor packages.",
18
- "Renders capacitor.config.json and .jskit/mobile-capacitor.md from config.mobile.",
19
- "Runs npm install and then cap add android unless --dry-run is used.",
20
- "Use --devlinks only when you want npm run devlinks to relink the app after install.",
21
- "If android/ already exists, the Capacitor CLI add step is skipped."
22
- ])
23
- }),
24
2
  sync: Object.freeze({
25
3
  name: "sync",
26
4
  summary: "Build the JSKIT web client and sync the Android Capacitor shell.",
27
- usage: "jskit mobile sync android [--dry-run] [--devlinks]",
5
+ usage: "jskit mobile android sync [--dry-run] [--devlinks]",
28
6
  options: Object.freeze([
29
7
  Object.freeze({
30
8
  label: "--devlinks",
@@ -38,13 +16,13 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
38
16
  defaults: Object.freeze([
39
17
  "Runs npm run build so dist/ matches the current JSKIT web client.",
40
18
  "Runs cap sync android after the frontend build succeeds.",
41
- "Requires capacitor.config.json and android/ from jskit mobile add capacitor."
19
+ "Requires capacitor.config.json and android/ from jskit add package @jskit-ai/mobile-capacitor."
42
20
  ])
43
21
  }),
44
22
  run: Object.freeze({
45
23
  name: "run",
46
24
  summary: "Launch the Android Capacitor shell for the current app.",
47
- usage: "jskit mobile run android [--target <device-id>] [--dry-run]",
25
+ usage: "jskit mobile android run [--target <device-id>] [--dry-run]",
48
26
  options: Object.freeze([
49
27
  Object.freeze({
50
28
  label: "--target <device-id>",
@@ -62,8 +40,8 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
62
40
  }),
63
41
  dev: Object.freeze({
64
42
  name: "dev",
65
- summary: "Run the local Android phone workflow: sync, install/run, then create the adb reverse tunnel.",
66
- usage: "jskit mobile dev android [--target <device-id>]",
43
+ summary: "Shortcut to run sync, tunnel, run in this order.",
44
+ usage: "jskit mobile android dev [--target <device-id>]",
67
45
  options: Object.freeze([
68
46
  Object.freeze({
69
47
  label: "--target <device-id>",
@@ -71,15 +49,15 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
71
49
  })
72
50
  ]),
73
51
  defaults: Object.freeze([
74
- "Runs jskit mobile sync android first.",
75
- "Runs jskit mobile run android --target <device-id> without re-syncing a second time.",
76
- "Runs jskit mobile tunnel android --target <device-id> last so local backend traffic reaches your laptop."
52
+ "jskit mobile android sync",
53
+ "jskit mobile android tunnel",
54
+ "jskit mobile android run"
77
55
  ])
78
56
  }),
79
57
  devices: Object.freeze({
80
58
  name: "devices",
81
59
  summary: "List Android devices currently visible to adb.",
82
- usage: "jskit mobile devices android",
60
+ usage: "jskit mobile android devices",
83
61
  options: Object.freeze([]),
84
62
  defaults: Object.freeze([
85
63
  "Runs adb devices -l and prints the currently connected Android targets."
@@ -88,11 +66,11 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
88
66
  tunnel: Object.freeze({
89
67
  name: "tunnel",
90
68
  summary: "Create and verify an adb reverse tunnel for local Android testing.",
91
- usage: "jskit mobile tunnel android --target <device-id> [--port <port>]",
69
+ usage: "jskit mobile android tunnel [--target <device-id>] [--port <port>]",
92
70
  options: Object.freeze([
93
71
  Object.freeze({
94
72
  label: "--target <device-id>",
95
- description: "Required adb device serial to tunnel to."
73
+ description: "Optional adb device serial. If omitted, uses the first device from adb devices -l."
96
74
  }),
97
75
  Object.freeze({
98
76
  label: "--port <port>",
@@ -107,11 +85,11 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
107
85
  restart: Object.freeze({
108
86
  name: "restart",
109
87
  summary: "Clear app data and cold-start the Android shell on a chosen device.",
110
- usage: "jskit mobile restart android --target <device-id>",
88
+ usage: "jskit mobile android restart [--target <device-id>]",
111
89
  options: Object.freeze([
112
90
  Object.freeze({
113
91
  label: "--target <device-id>",
114
- description: "Required adb device serial to restart."
92
+ description: "Optional adb device serial. If omitted, uses the first device from adb devices -l."
115
93
  })
116
94
  ]),
117
95
  defaults: Object.freeze([
@@ -122,7 +100,7 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
122
100
  build: Object.freeze({
123
101
  name: "build",
124
102
  summary: "Build a release Android App Bundle for the current app.",
125
- usage: "jskit mobile build android [--dry-run]",
103
+ usage: "jskit mobile android build [--dry-run]",
126
104
  options: Object.freeze([
127
105
  Object.freeze({
128
106
  label: "--dry-run",
@@ -137,7 +115,7 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
137
115
  doctor: Object.freeze({
138
116
  name: "doctor",
139
117
  summary: "Validate the Android Capacitor shell wiring for the current app.",
140
- usage: "jskit mobile doctor",
118
+ usage: "jskit mobile android doctor",
141
119
  options: Object.freeze([]),
142
120
  defaults: Object.freeze([
143
121
  "Checks config.mobile, capacitor.config.json, android/, and the managed AndroidManifest deep-link filter."
@@ -146,8 +124,25 @@ const MOBILE_COMMAND_DEFINITIONS = Object.freeze({
146
124
  });
147
125
 
148
126
  function listMobileCommandDefinitions() {
127
+ const order = new Map([
128
+ ["dev", 0],
129
+ ["sync", 1],
130
+ ["tunnel", 2],
131
+ ["run", 3],
132
+ ["restart", 4],
133
+ ["build", 5],
134
+ ["devices", 6],
135
+ ["doctor", 7]
136
+ ]);
149
137
  return Object.values(MOBILE_COMMAND_DEFINITIONS)
150
- .sort((left, right) => left.name.localeCompare(right.name));
138
+ .sort((left, right) => {
139
+ const leftOrder = order.get(left.name) ?? Number.MAX_SAFE_INTEGER;
140
+ const rightOrder = order.get(right.name) ?? Number.MAX_SAFE_INTEGER;
141
+ if (leftOrder !== rightOrder) {
142
+ return leftOrder - rightOrder;
143
+ }
144
+ return left.name.localeCompare(right.name);
145
+ });
151
146
  }
152
147
 
153
148
  function resolveMobileCommandDefinition(rawName = "") {
@@ -168,7 +163,7 @@ function buildMobileCommandOptionMeta(subcommandName = "") {
168
163
  return optionMeta;
169
164
  }
170
165
 
171
- if (definition.name === "add" || definition.name === "sync" || definition.name === "run" || definition.name === "build") {
166
+ if (definition.name === "sync" || definition.name === "run" || definition.name === "build") {
172
167
  optionMeta["dry-run"] = { inputType: "flag" };
173
168
  }
174
169
  if (definition.name === "run") {
@@ -619,7 +619,7 @@ async function assertCapacitorShellInstalled({ ctx, appRoot }) {
619
619
 
620
620
  if (missingPaths.length > 0) {
621
621
  throw ctx.createCliError(
622
- `Capacitor Android shell is not installed for this app. Missing: ${missingPaths.join(", ")}. Run jskit mobile add capacitor first.`
622
+ `Capacitor Android shell is not installed for this app. Missing: ${missingPaths.join(", ")}. Run jskit add package @jskit-ai/mobile-capacitor first.`
623
623
  );
624
624
  }
625
625
  }
@@ -678,7 +678,7 @@ async function ensureAndroidManifestDeepLinks({
678
678
  const manifestPath = pathModule.join(appRoot, ANDROID_MANIFEST_RELATIVE_PATH);
679
679
  if (!(await fileExists(manifestPath))) {
680
680
  throw createCliError(
681
- `Capacitor Android shell is missing ${normalizeRelativePath(appRoot, manifestPath)}. Run jskit mobile add capacitor first.`
681
+ `Capacitor Android shell is missing ${normalizeRelativePath(appRoot, manifestPath)}. Run jskit add package @jskit-ai/mobile-capacitor first.`
682
682
  );
683
683
  }
684
684
 
@@ -722,7 +722,7 @@ async function collectAndroidNativeShellIdentityIssues({ ctx, appRoot } = {}) {
722
722
  const expectedSource = renderer(currentSource, nativeConfig);
723
723
  if (currentSource !== expectedSource) {
724
724
  issues.push(
725
- `${normalizeRelativePath(appRoot, absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile sync android to refresh the Android shell.`
725
+ `${normalizeRelativePath(appRoot, absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile android sync to refresh the Android shell.`
726
726
  );
727
727
  }
728
728
  };
@@ -757,7 +757,7 @@ async function collectAndroidNativeShellIdentityIssues({ ctx, appRoot } = {}) {
757
757
  currentMainActivitySource !== expectedMainActivitySource
758
758
  ) {
759
759
  issues.push(
760
- `${normalizeRelativePath(appRoot, mainActivityEntry.absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile sync android to refresh the Android shell.`
760
+ `${normalizeRelativePath(appRoot, mainActivityEntry.absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile android sync to refresh the Android shell.`
761
761
  );
762
762
  }
763
763
 
@@ -783,7 +783,7 @@ async function ensureAndroidNativeShellIdentity({
783
783
  const absolutePath = pathModule.join(appRoot, relativePath);
784
784
  if (!(await fileExists(absolutePath))) {
785
785
  throw createCliError(
786
- `Capacitor Android shell is missing ${normalizeRelativePath(appRoot, absolutePath)}. Run jskit mobile add capacitor first.`
786
+ `Capacitor Android shell is missing ${normalizeRelativePath(appRoot, absolutePath)}. Run jskit add package @jskit-ai/mobile-capacitor first.`
787
787
  );
788
788
  }
789
789
  const currentSource = await readFile(absolutePath, "utf8");
@@ -807,7 +807,7 @@ async function ensureAndroidNativeShellIdentity({
807
807
 
808
808
  const mainActivityEntry = await resolveAndroidMainActivityEntry(appRoot);
809
809
  if (!mainActivityEntry) {
810
- throw createCliError("Capacitor Android shell is missing MainActivity.java or MainActivity.kt. Run jskit mobile add capacitor first.");
810
+ throw createCliError("Capacitor Android shell is missing MainActivity.java or MainActivity.kt. Run jskit add package @jskit-ai/mobile-capacitor first.");
811
811
  }
812
812
 
813
813
  const currentMainActivitySource = await readFile(mainActivityEntry.absolutePath, "utf8");
@@ -23,6 +23,7 @@ function parseArgs(argv, { createCliError } = {}) {
23
23
  verbose: false,
24
24
  json: false,
25
25
  all: false,
26
+ concrete: false,
26
27
  help: true,
27
28
  inlineOptions: {}
28
29
  },
@@ -49,6 +50,7 @@ function parseArgs(argv, { createCliError } = {}) {
49
50
  verbose: false,
50
51
  json: false,
51
52
  all: false,
53
+ concrete: false,
52
54
  help: false,
53
55
  inlineOptions: {}
54
56
  };
@@ -107,6 +109,10 @@ function parseArgs(argv, { createCliError } = {}) {
107
109
  options.all = true;
108
110
  continue;
109
111
  }
112
+ if (token === "--concrete") {
113
+ options.concrete = true;
114
+ continue;
115
+ }
110
116
  if (token === "--help" || token === "-h") {
111
117
  options.help = true;
112
118
  continue;
@@ -54,7 +54,8 @@ function createCommandHandlerDeps(deps = {}) {
54
54
  removeManagedViteProxyEntries: deps.removeManagedViteProxyEntries,
55
55
  hashBuffer: deps.hashBuffer,
56
56
  rm: deps.rm,
57
- discoverShellOutletTargetsFromApp: deps.discoverShellOutletTargetsFromApp
57
+ discoverShellOutletTargetsFromApp: deps.discoverShellOutletTargetsFromApp,
58
+ discoverPlacementTopologyFromApp: deps.discoverPlacementTopologyFromApp
58
59
  };
59
60
  }
60
61
 
@@ -173,20 +173,24 @@ const COMMAND_DESCRIPTORS = Object.freeze({
173
173
  aliases: Object.freeze([]),
174
174
  showInOverview: true,
175
175
  summary: "Run JSKIT-managed mobile-shell helpers.",
176
- minimalUse: "jskit mobile add capacitor",
176
+ minimalUse: "jskit mobile android dev",
177
177
  parameters: Object.freeze([
178
+ Object.freeze({
179
+ name: "<platform>",
180
+ description: "Currently only android is supported."
181
+ }),
178
182
  Object.freeze({
179
183
  name: "<subcommand>",
180
- description: "add (more mobile helpers will live here as Stage 1 expands)."
184
+ description: "dev | devices | sync | tunnel | restart | run | build | doctor."
181
185
  })
182
186
  ]),
183
187
  defaults: Object.freeze([
184
- "The first supported flow is jskit mobile add capacitor.",
185
- "Use jskit mobile <subcommand> help for subcommand-specific usage.",
186
- "--dry-run is accepted by jskit mobile add/sync/run/build.",
187
- "--devlinks runs npm run --if-present devlinks after install/sync maintenance for development-only relinking."
188
+ "Install the shell first with jskit add package @jskit-ai/mobile-capacitor.",
189
+ "Use jskit mobile <platform> help for platform-specific usage.",
190
+ "--dry-run is accepted by jskit mobile android sync/run/build.",
191
+ "--devlinks runs npm run --if-present devlinks after jskit mobile android sync maintenance for development-only relinking."
188
192
  ]),
189
- fullUse: "jskit mobile <subcommand> [help] [--dry-run] [--<option> <value>...]",
193
+ fullUse: "jskit mobile <platform> <subcommand> [help] [--dry-run] [--<option> <value>...]",
190
194
  showHelpOnBareInvocation: true,
191
195
  handlerName: "commandMobile",
192
196
  allowedFlagKeys: Object.freeze(["dryRun", "devlinks"]),
@@ -313,14 +317,14 @@ const COMMAND_DESCRIPTORS = Object.freeze({
313
317
  minimalUse: "jskit list-placements",
314
318
  parameters: Object.freeze([]),
315
319
  defaults: Object.freeze([
316
- "Discovers placement outlets from app Vue ShellOutlet tags and route meta.",
317
- "Includes placement outlets contributed by installed package metadata.",
318
- "Shows plain text by default; use --json for structured output."
320
+ "Shows semantic placement targets from app placement topology by default.",
321
+ "Use --concrete to inspect low-level ShellOutlet recipients.",
322
+ "Use --all to show both semantic placements and concrete recipients."
319
323
  ]),
320
- fullUse: "jskit list-placements [--json]",
324
+ fullUse: "jskit list-placements [--concrete] [--all] [--json]",
321
325
  showHelpOnBareInvocation: false,
322
326
  handlerName: "commandListPlacements",
323
- allowedFlagKeys: Object.freeze(["json"]),
327
+ allowedFlagKeys: Object.freeze(["concrete", "all", "json"]),
324
328
  inlineOptionMode: "none",
325
329
  allowedValueOptionNames: Object.freeze([])
326
330
  }),
@@ -5,7 +5,10 @@ import {
5
5
  writeFile
6
6
  } from "node:fs/promises";
7
7
  import path from "node:path";
8
- import { discoverShellOutletTargetsFromApp } from "@jskit-ai/kernel/server/support";
8
+ import {
9
+ discoverPlacementTopologyFromApp,
10
+ discoverShellOutletTargetsFromApp
11
+ } from "@jskit-ai/kernel/server/support";
9
12
  import { createCliError } from "../shared/cliError.js";
10
13
  import {
11
14
  createColorFormatter,
@@ -146,7 +149,8 @@ const commandHandlers = createCommandHandlers(
146
149
  removeManagedViteProxyEntries,
147
150
  hashBuffer,
148
151
  rm,
149
- discoverShellOutletTargetsFromApp
152
+ discoverShellOutletTargetsFromApp,
153
+ discoverPlacementTopologyFromApp
150
154
  })
151
155
  );
152
156