@namiml/cli 3.4.11-dev.202609221916 → 3.4.11-dev.202609222222
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/README.md +18 -2
- package/dist/adapters/file-storage.adapter.js +13 -4
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/terminal-device.adapter.js +24 -3
- package/dist/cli.js +2 -0
- package/dist/commands/configure.js +7 -2
- package/dist/commands/devices.js +21 -0
- package/dist/commands/shell.js +1 -1
- package/dist/devices/presets.js +83 -0
- package/dist/devices/table.js +59 -0
- package/dist/program.js +11 -0
- package/dist/screens/ConfigureScreen.js +2 -2
- package/dist/screens/PickDevice.js +29 -0
- package/dist/screens/SelectionHeader.js +2 -2
- package/dist/screens/Shell.js +38 -9
- package/dist/sdk/configure.js +16 -2
- package/dist/version.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -8,13 +8,29 @@ npm install -g @namiml/cli # or install the `nami` command
|
|
|
8
8
|
nami --version
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Requires Node 20+.
|
|
11
|
+
Requires Node 20+.
|
|
12
12
|
|
|
13
13
|
`@namiml/cli` is versioned in lockstep with the Nami SDKs — `nami --version` tells you exactly which SDK release it is pinned to.
|
|
14
14
|
|
|
15
|
+
## Standalone executables
|
|
16
|
+
|
|
17
|
+
Single-file builds for macOS, Linux and Windows need no Node install. Replace `<version>` with the release you want:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
https://packages.namiml.com/NamiSDK/CLI/<version>/nami-<version>-darwin-arm64
|
|
21
|
+
https://packages.namiml.com/NamiSDK/CLI/<version>/nami-<version>-darwin-x64
|
|
22
|
+
https://packages.namiml.com/NamiSDK/CLI/<version>/nami-<version>-linux-x64
|
|
23
|
+
https://packages.namiml.com/NamiSDK/CLI/<version>/nami-<version>-windows-x64.exe
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
curl -fsSLO https://packages.namiml.com/NamiSDK/CLI/3.5.0/nami-3.5.0-darwin-arm64
|
|
28
|
+
chmod +x nami-3.5.0-darwin-arm64 && ./nami-3.5.0-darwin-arm64 --version
|
|
29
|
+
```
|
|
30
|
+
|
|
15
31
|
## Status
|
|
16
32
|
|
|
17
|
-
Early access. The current release is a scaffold; the test-app screens, onboarding and implementation checks are landing release by release.
|
|
33
|
+
Early access. The current release is a scaffold; the test-app screens, onboarding and implementation checks are landing release by release.
|
|
18
34
|
|
|
19
35
|
## License
|
|
20
36
|
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
const DEBOUNCE_MS = 50;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const SAFE = /^[A-Za-z0-9_-]+$/;
|
|
5
|
+
/**
|
|
6
|
+
* Storage file for one App Platform ID — and, when a device preset is in play,
|
|
7
|
+
* one per device under it (`storage/<appPlatformId>/<deviceId>.json`): an iPhone
|
|
8
|
+
* session and an iPad session are different SDK devices. Throws on path-traversal attempts.
|
|
9
|
+
*/
|
|
10
|
+
export function storagePathFor(appPlatformId, configDir, deviceId) {
|
|
11
|
+
if (!SAFE.test(appPlatformId))
|
|
7
12
|
throw new Error(`Refusing to build a storage path from "${appPlatformId}".`);
|
|
8
|
-
|
|
13
|
+
if (deviceId === undefined)
|
|
14
|
+
return join(configDir, "storage", `${appPlatformId}.json`);
|
|
15
|
+
if (!SAFE.test(deviceId))
|
|
16
|
+
throw new Error(`Refusing to build a storage path from device "${deviceId}".`);
|
|
17
|
+
return join(configDir, "storage", appPlatformId, `${deviceId}.json`);
|
|
9
18
|
}
|
|
10
19
|
/**
|
|
11
20
|
* Synchronous IStorageAdapter backed by one JSON file per App Platform ID, so
|
package/dist/adapters/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { TerminalPurchaseAdapter } from "./terminal-purchase.adapter.js";
|
|
|
7
7
|
export function registerTerminalAdapters(options = {}) {
|
|
8
8
|
const adapters = {
|
|
9
9
|
storage: options.storage ?? new TerminalStorageAdapter(),
|
|
10
|
-
device: new TerminalDeviceAdapter(),
|
|
10
|
+
device: new TerminalDeviceAdapter(options.device),
|
|
11
11
|
ui: new TerminalUIAdapter(),
|
|
12
12
|
purchase: new TerminalPurchaseAdapter(),
|
|
13
13
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { NAMI_SDK_PACKAGE_VERSION } from "@namiml/sdk-core";
|
|
4
|
+
import { deviceOs } from "../devices/presets.js";
|
|
5
|
+
const TV_SCALE = { "720p": 0.67, "1080p": 1, "4k": 2 };
|
|
4
6
|
const SDK_CLIENT = "cli";
|
|
5
7
|
function terminalLanguage() {
|
|
6
8
|
const raw = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || "en";
|
|
@@ -8,8 +10,13 @@ function terminalLanguage() {
|
|
|
8
10
|
return !tag || tag === "C" || tag === "POSIX" ? "en" : tag;
|
|
9
11
|
}
|
|
10
12
|
export class TerminalDeviceAdapter {
|
|
13
|
+
preset;
|
|
14
|
+
/** Without a preset the adapter describes the host machine (a desktop). */
|
|
15
|
+
constructor(preset) {
|
|
16
|
+
this.preset = preset;
|
|
17
|
+
}
|
|
11
18
|
getDeviceData() {
|
|
12
|
-
|
|
19
|
+
const base = {
|
|
13
20
|
os_name: os.platform(),
|
|
14
21
|
os_version: os.release(),
|
|
15
22
|
browser_name: "",
|
|
@@ -20,17 +27,31 @@ export class TerminalDeviceAdapter {
|
|
|
20
27
|
extended_platform: "node",
|
|
21
28
|
extended_platform_version: process.versions.node,
|
|
22
29
|
};
|
|
30
|
+
if (!this.preset)
|
|
31
|
+
return base;
|
|
32
|
+
const { device, platformType } = this.preset;
|
|
33
|
+
return {
|
|
34
|
+
...base,
|
|
35
|
+
os_name: deviceOs(device, platformType),
|
|
36
|
+
os_version: "",
|
|
37
|
+
device_model: device.displayName,
|
|
38
|
+
form_factor: device.type,
|
|
39
|
+
browser_name: device.browser ?? "",
|
|
40
|
+
};
|
|
23
41
|
}
|
|
24
42
|
getDeviceFormFactor() {
|
|
25
|
-
return "desktop";
|
|
43
|
+
return this.preset?.device.type ?? "desktop";
|
|
26
44
|
}
|
|
27
45
|
getDeviceScaleFactor() {
|
|
28
|
-
|
|
46
|
+
const quality = this.preset?.device.tvQuality;
|
|
47
|
+
return quality ? TV_SCALE[quality] ?? 1 : 1;
|
|
29
48
|
}
|
|
30
49
|
generateUUID() {
|
|
31
50
|
return randomUUID();
|
|
32
51
|
}
|
|
33
52
|
getScreenInfo() {
|
|
53
|
+
if (this.preset)
|
|
54
|
+
return { width: this.preset.device.width, height: this.preset.device.height, scale: this.getDeviceScaleFactor() };
|
|
34
55
|
return { width: process.stdout.columns ?? 80, height: process.stdout.rows ?? 24, scale: 1 };
|
|
35
56
|
}
|
|
36
57
|
getLanguage() {
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { shell } from "./commands/shell.js";
|
|
|
13
13
|
import { configure } from "./commands/configure.js";
|
|
14
14
|
import { config } from "./commands/config.js";
|
|
15
15
|
import { onboarding } from "./commands/onboarding.js";
|
|
16
|
+
import { devices } from "./commands/devices.js";
|
|
16
17
|
const program = createProgram({
|
|
17
18
|
shell: async (opts) => shell(opts, await buildContext(opts.host)),
|
|
18
19
|
demo: () => { render(_jsx(App, {})); },
|
|
@@ -24,5 +25,6 @@ const program = createProgram({
|
|
|
24
25
|
configure: async (opts) => configure(opts, await buildContext(opts.host)),
|
|
25
26
|
config: async (opts) => config(opts, await buildContext(opts.host)),
|
|
26
27
|
onboarding: async (opts) => onboarding(opts, await buildContext(opts.host)),
|
|
28
|
+
devices: async (opts) => devices(opts, await buildContext(opts.host)),
|
|
27
29
|
});
|
|
28
30
|
await program.parseAsync(process.argv);
|
|
@@ -4,12 +4,17 @@ import { SDK_DISABLED_MESSAGE, VerifyError, verifyAppPlatform } from "../sdk/ver
|
|
|
4
4
|
export async function configure(opts, ctx, deps = {}) {
|
|
5
5
|
const verify = deps.verify ?? verifyAppPlatform;
|
|
6
6
|
const run = deps.configure ?? configureSdk;
|
|
7
|
+
if (opts.device && !opts.platformType) {
|
|
8
|
+
ctx.err("--device needs --platform-type <apple|google|amazon_iap|roku_pay|web> so the CLI knows which device family to use.");
|
|
9
|
+
return 2;
|
|
10
|
+
}
|
|
7
11
|
let result;
|
|
8
12
|
try {
|
|
9
13
|
const verified = await verify(ctx.host.baseUrl, opts.appPlatformId, deps.fetch ?? globalThis.fetch);
|
|
10
14
|
if (verified.kind === "sdk_disabled")
|
|
11
15
|
ctx.err(SDK_DISABLED_MESSAGE);
|
|
12
|
-
|
|
16
|
+
const device = opts.platformType ? { platformType: opts.platformType, id: opts.device } : undefined;
|
|
17
|
+
result = await run({ host: ctx.host, appPlatformId: opts.appPlatformId, device }, { ...deps, verified });
|
|
13
18
|
}
|
|
14
19
|
catch (error) {
|
|
15
20
|
if (error instanceof VerifyError) {
|
|
@@ -22,7 +27,7 @@ export async function configure(opts, ctx, deps = {}) {
|
|
|
22
27
|
ctx.out(JSON.stringify(result, null, 2));
|
|
23
28
|
}
|
|
24
29
|
else {
|
|
25
|
-
ctx.out(`Configured ${result.appPlatformId} on ${result.host}: ${result.configureState}${result.sdkEnabled ? "" : " (SDK disabled)"}`);
|
|
30
|
+
ctx.out(`Configured ${result.appPlatformId} on ${result.host}: ${result.configureState}${result.sdkEnabled ? "" : " (SDK disabled)"}${result.device ? ` · ${result.device.displayName}` : ""}`);
|
|
26
31
|
ctx.out(`placements ${result.counts.placements} · products ${result.counts.products} · paywalls ${result.counts.paywalls}${result.deviceId ? ` · device ${result.deviceId}` : ""}`);
|
|
27
32
|
}
|
|
28
33
|
return result.sdkInitialized ? 0 : 1;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PLATFORM_TYPES, devicesFor, presetsFor } from "../devices/presets.js";
|
|
2
|
+
import { renderTable } from "./list-output.js";
|
|
3
|
+
/** `nami devices --platform-type <type>` — the presets and the full previewer list for a platform type. */
|
|
4
|
+
export async function devices(opts, ctx) {
|
|
5
|
+
if (!PLATFORM_TYPES.includes(opts.platformType)) {
|
|
6
|
+
ctx.err(`Unknown platform type "${opts.platformType}". Use one of: ${PLATFORM_TYPES.join(", ")}.`);
|
|
7
|
+
return 2;
|
|
8
|
+
}
|
|
9
|
+
const presets = presetsFor(opts.platformType);
|
|
10
|
+
const all = devicesFor(opts.platformType);
|
|
11
|
+
if (opts.json) {
|
|
12
|
+
ctx.out(JSON.stringify({ platformType: opts.platformType, presets, devices: all }, null, 2));
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
ctx.out(`Presets for ${opts.platformType} (pass the label or id to --device):`);
|
|
16
|
+
ctx.out(renderTable(presets.map((p) => ({ preset: p.label, formFactor: p.formFactor, device: p.deviceId })), ["preset", "formFactor", "device"]));
|
|
17
|
+
ctx.out("");
|
|
18
|
+
ctx.out(`All ${all.length} devices:`);
|
|
19
|
+
ctx.out(renderTable(all.map((d) => ({ id: d.id, name: d.displayName, type: d.type, size: `${d.width}×${d.height}` })), ["id", "name", "type", "size"]));
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
package/dist/commands/shell.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function shell(opts, ctx) {
|
|
|
10
10
|
return 0;
|
|
11
11
|
}
|
|
12
12
|
let code = 0;
|
|
13
|
-
const app = render(_jsx(Shell, { ctx: ctx, appQuery: opts.app, platformQuery: opts.platform, onExit: (c) => { code = c; } }));
|
|
13
|
+
const app = render(_jsx(Shell, { ctx: ctx, appQuery: opts.app, platformQuery: opts.platform, deviceQuery: opts.device, onExit: (c) => { code = c; } }));
|
|
14
14
|
await app.waitUntilExit();
|
|
15
15
|
return code;
|
|
16
16
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolve } from "../control-center/resolve.js";
|
|
2
|
+
import { PREVIEW_DEVICES, PREVIEW_DEVICE_BY_ID } from "./table.js";
|
|
3
|
+
/** Billing platform types the Control Center uses (`PlatformSerializer.type`). */
|
|
4
|
+
export const PLATFORM_TYPES = ["apple", "google", "amazon_iap", "roku_pay", "web"];
|
|
5
|
+
/** Copied from frontend `utils/paywall.ts` `platformCompatibleWithFormFactor`. */
|
|
6
|
+
export function platformCompatibleWithFormFactor(platformType, formFactor) {
|
|
7
|
+
if (platformType === "roku_pay" || platformType === "amazon_iap")
|
|
8
|
+
return formFactor === "television";
|
|
9
|
+
if (platformType === "apple" || platformType === "google")
|
|
10
|
+
return formFactor === "television" || formFactor === "phone" || formFactor === "tablet";
|
|
11
|
+
if (platformType === "web")
|
|
12
|
+
return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
/** Copied from frontend `PaywallPreview/utils.ts` `getDefaultDeviceForFormFactor`. */
|
|
16
|
+
export function defaultDeviceFor(formFactor, platformType) {
|
|
17
|
+
if (formFactor === "television")
|
|
18
|
+
return "tv_1080p";
|
|
19
|
+
if (formFactor === "desktop")
|
|
20
|
+
return "desktop_1280x1024";
|
|
21
|
+
if (formFactor === "tablet")
|
|
22
|
+
return platformType === "google" ? "samsung_galaxy_tab_s9" : "ipadair_13";
|
|
23
|
+
if (platformType === "google")
|
|
24
|
+
return "google_pixel_678";
|
|
25
|
+
if (platformType === "web")
|
|
26
|
+
return "iphone16_chrome";
|
|
27
|
+
return "iphone16";
|
|
28
|
+
}
|
|
29
|
+
const preset = (label, formFactor, platformType) => ({ label, formFactor, deviceId: defaultDeviceFor(formFactor, platformType) });
|
|
30
|
+
/** Tier 1: the coarse choices a developer sees first, per platform type. */
|
|
31
|
+
export const PRESETS = {
|
|
32
|
+
apple: [preset("iPhone", "phone", "apple"), preset("iPad", "tablet", "apple"), preset("Apple TV", "television", "apple")],
|
|
33
|
+
google: [preset("Android Phone", "phone", "google"), preset("Android Tablet", "tablet", "google"), preset("Android TV", "television", "google")],
|
|
34
|
+
amazon_iap: [preset("Fire TV", "television", "amazon_iap")],
|
|
35
|
+
roku_pay: [preset("Roku", "television", "roku_pay")],
|
|
36
|
+
web: [preset("Mobile", "phone", "web"), preset("Tablet", "tablet", "web"), preset("Desktop", "desktop", "web"), preset("TV", "television", "web")],
|
|
37
|
+
};
|
|
38
|
+
export const presetsFor = (platformType) => PRESETS[platformType] ?? [];
|
|
39
|
+
/** Tier 2: every previewer device that fits the platform's form factors and family. */
|
|
40
|
+
export function devicesFor(platformType) {
|
|
41
|
+
return PREVIEW_DEVICES.filter((d) => {
|
|
42
|
+
if (!platformCompatibleWithFormFactor(platformType, d.type))
|
|
43
|
+
return false;
|
|
44
|
+
if (d.type === "television")
|
|
45
|
+
return true;
|
|
46
|
+
if (platformType === "apple")
|
|
47
|
+
return d.os === "apple";
|
|
48
|
+
if (platformType === "google")
|
|
49
|
+
return d.os === "google";
|
|
50
|
+
if (platformType === "web")
|
|
51
|
+
return d.os === "web" || d.type === "desktop";
|
|
52
|
+
return false;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** id → preset label → display name (exact, then unique substring) — else ResolveError. */
|
|
56
|
+
export function resolveDevice(platformType, query) {
|
|
57
|
+
const q = query.trim();
|
|
58
|
+
const candidates = devicesFor(platformType);
|
|
59
|
+
const byId = candidates.find((d) => d.id === q);
|
|
60
|
+
if (byId)
|
|
61
|
+
return byId;
|
|
62
|
+
const byPreset = presetsFor(platformType).find((p) => p.label.toLowerCase() === q.toLowerCase());
|
|
63
|
+
if (byPreset)
|
|
64
|
+
return PREVIEW_DEVICE_BY_ID.get(byPreset.deviceId);
|
|
65
|
+
const hit = resolve(candidates.map((d) => ({ id: d.id, name: d.displayName })), q, "device");
|
|
66
|
+
return PREVIEW_DEVICE_BY_ID.get(hit.id);
|
|
67
|
+
}
|
|
68
|
+
const BROWSER_LABEL = { safari: "Safari", apple_chrome: "Chrome", google_chrome: "Chrome" };
|
|
69
|
+
/** Human OS label for the device payload, from the device and the platform it is used with. */
|
|
70
|
+
export function deviceOs(device, platformType) {
|
|
71
|
+
if (device.type === "television") {
|
|
72
|
+
return { apple: "tvOS", google: "Android TV", amazon_iap: "Fire OS", roku_pay: "Roku OS" }[platformType] ?? "Web";
|
|
73
|
+
}
|
|
74
|
+
if (platformType === "web" || device.os === "web")
|
|
75
|
+
return device.browser ? BROWSER_LABEL[device.browser] ?? "Web" : "Web";
|
|
76
|
+
if (device.os === "apple")
|
|
77
|
+
return device.type === "tablet" ? "iPadOS" : "iOS";
|
|
78
|
+
if (device.os === "google")
|
|
79
|
+
return "Android";
|
|
80
|
+
return "Web";
|
|
81
|
+
}
|
|
82
|
+
export const toSelection = (device) => ({ id: device.id, displayName: device.displayName, formFactor: device.type });
|
|
83
|
+
export const deviceById = (id) => PREVIEW_DEVICE_BY_ID.get(id);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED — do not hand-edit.
|
|
3
|
+
*
|
|
4
|
+
* Copied from nami-platform: services/frontend/src/api/types/device.types.ts
|
|
5
|
+
* (`PreviewDeviceMeta`), the Control Center previewer's device list. Extracted 2026-09-22.
|
|
6
|
+
* Regenerate by re-running the extraction in NAM-4505's plan; `tests/unit/devices/presets.test.ts`
|
|
7
|
+
* enforces the platform/form-factor compatibility rule over every entry.
|
|
8
|
+
*/
|
|
9
|
+
export const PREVIEW_DEVICES = [
|
|
10
|
+
{ id: "desktop_1920x1080", type: "desktop", os: null, brand: null, browser: null, displayName: "Desktop", width: 1920, height: 1080, tvQuality: null },
|
|
11
|
+
{ id: "desktop_1440x900", type: "desktop", os: null, brand: null, browser: null, displayName: "Laptop Lg", width: 1440, height: 900, tvQuality: null },
|
|
12
|
+
{ id: "desktop_1280x1024", type: "desktop", os: null, brand: null, browser: null, displayName: "Laptop Md", width: 1280, height: 1024, tvQuality: null },
|
|
13
|
+
{ id: "desktop_1024x768", type: "desktop", os: null, brand: null, browser: null, displayName: "Laptop Sm", width: 1024, height: 768, tvQuality: null },
|
|
14
|
+
{ id: "tv_4k", type: "television", os: null, brand: null, browser: null, displayName: "4K TV", width: 3840, height: 2160, tvQuality: "4k" },
|
|
15
|
+
{ id: "tv_1080p", type: "television", os: null, brand: null, browser: null, displayName: "1080p TV", width: 1920, height: 1080, tvQuality: "1080p" },
|
|
16
|
+
{ id: "tv_720p", type: "television", os: null, brand: null, browser: null, displayName: "720p TV", width: 1280, height: 720, tvQuality: "720p" },
|
|
17
|
+
{ id: "iphone16", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 16", width: 393, height: 852, tvQuality: null },
|
|
18
|
+
{ id: "iphone16_chrome", type: "phone", os: "web", brand: null, browser: "apple_chrome", displayName: "Chrome on iPhone 16", width: 393, height: 852, tvQuality: null },
|
|
19
|
+
{ id: "iphone16_safari", type: "phone", os: "web", brand: null, browser: "safari", displayName: "Safari on iPhone 16", width: 393, height: 852, tvQuality: null },
|
|
20
|
+
{ id: "iphone16_pro", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 16 Pro", width: 402, height: 874, tvQuality: null },
|
|
21
|
+
{ id: "iphone16_plus", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 16 Plus", width: 430, height: 932, tvQuality: null },
|
|
22
|
+
{ id: "iphone16_plus_safari", type: "phone", os: "web", brand: null, browser: "safari", displayName: "Safari on iPhone 16 Plus", width: 430, height: 932, tvQuality: null },
|
|
23
|
+
{ id: "iphone16_plus_chrome", type: "phone", os: "web", brand: null, browser: "apple_chrome", displayName: "Chrome on iPhone 16 Plus", width: 430, height: 932, tvQuality: null },
|
|
24
|
+
{ id: "iphone16_pro_max", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 16 Pro Max", width: 440, height: 956, tvQuality: null },
|
|
25
|
+
{ id: "iphone16_pro_max_chrome", type: "phone", os: "web", brand: null, browser: "apple_chrome", displayName: "Chrome on iPhone 16 Pro Max", width: 440, height: 956, tvQuality: null },
|
|
26
|
+
{ id: "iphone16_pro_max_safari", type: "phone", os: "web", brand: null, browser: "safari", displayName: "Safari on iPhone 16 Pro Max", width: 440, height: 956, tvQuality: null },
|
|
27
|
+
{ id: "iphone15", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 15", width: 390, height: 844, tvQuality: null },
|
|
28
|
+
{ id: "iphone1415_pro", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 15 Pro", width: 393, height: 852, tvQuality: null },
|
|
29
|
+
{ id: "iphone1415_pro_max", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 15 Pro Max", width: 430, height: 932, tvQuality: null },
|
|
30
|
+
{ id: "iphone121314", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 14", width: 390, height: 844, tvQuality: null },
|
|
31
|
+
{ id: "iphone121314_chrome", type: "phone", os: "web", brand: null, browser: "apple_chrome", displayName: "Chrome on iPhone 14", width: 390, height: 844, tvQuality: null },
|
|
32
|
+
{ id: "iphone121314_safari", type: "phone", os: "web", brand: null, browser: "safari", displayName: "Safari on iPhone 14", width: 390, height: 844, tvQuality: null },
|
|
33
|
+
{ id: "iphone1213_pro_max", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 13 Pro Max", width: 428, height: 926, tvQuality: null },
|
|
34
|
+
{ id: "iphone1213_mini", type: "phone", os: "apple", brand: null, browser: null, displayName: "iPhone 13 Mini", width: 375, height: 812, tvQuality: null },
|
|
35
|
+
{ id: "iphone1213_mini_chrome", type: "phone", os: "web", brand: null, browser: "apple_chrome", displayName: "Chrome on iPhone 13 Mini", width: 375, height: 812, tvQuality: null },
|
|
36
|
+
{ id: "iphone1213_mini_safari", type: "phone", os: "web", brand: null, browser: "safari", displayName: "Safari on iPhone 13 Mini", width: 375, height: 812, tvQuality: null },
|
|
37
|
+
{ id: "samsung_galaxy_s22", type: "phone", os: "google", brand: "samsung", browser: null, displayName: "Samsung Galaxy S22", width: 360, height: 780, tvQuality: null },
|
|
38
|
+
{ id: "samsung_galaxy_s22_chrome", type: "phone", os: "web", brand: "samsung", browser: "google_chrome", displayName: "Chrome on Samsung Galaxy S22", width: 360, height: 780, tvQuality: null },
|
|
39
|
+
{ id: "samsung_galaxy_s22_ultra", type: "phone", os: "google", brand: "samsung", browser: null, displayName: "Samsung Galaxy S22 Ultra", width: 384, height: 824, tvQuality: null },
|
|
40
|
+
{ id: "samsung_galaxy_s22_ultra_chrome", type: "phone", os: "web", brand: "samsung", browser: "google_chrome", displayName: "Chrome on Samsung Galaxy S22 Ultra", width: 384, height: 824, tvQuality: null },
|
|
41
|
+
{ id: "samsung_galaxy_s2021", type: "phone", os: "google", brand: "samsung", browser: null, displayName: "Samsung Galaxy S21", width: 360, height: 800, tvQuality: null },
|
|
42
|
+
{ id: "samsung_galaxy_s2021_plus", type: "phone", os: "google", brand: "samsung", browser: null, displayName: "Samsung Galaxy S21 Plus", width: 384, height: 854, tvQuality: null },
|
|
43
|
+
{ id: "samsung_galaxy_s2021_ultra", type: "phone", os: "google", brand: "samsung", browser: null, displayName: "Samsung Galaxy S21 Ultra", width: 412, height: 915, tvQuality: null },
|
|
44
|
+
{ id: "samsung_galaxy_s9", type: "phone", os: "google", brand: "samsung", browser: null, displayName: "Samsung Galaxy S9", width: 360, height: 740, tvQuality: null },
|
|
45
|
+
{ id: "google_pixel_678", type: "phone", os: "google", brand: "google", browser: null, displayName: "Google Pixel 9", width: 412, height: 915, tvQuality: null },
|
|
46
|
+
{ id: "google_pixel_678_chrome", type: "phone", os: "web", brand: "google", browser: "google_chrome", displayName: "Chrome on Google Pixel 9", width: 412, height: 915, tvQuality: null },
|
|
47
|
+
{ id: "google_pixel_8_pro", type: "phone", os: "google", brand: "google", browser: null, displayName: "Google Pixel 9 Pro", width: 448, height: 998, tvQuality: null },
|
|
48
|
+
{ id: "google_pixel_8_pro_chrome", type: "phone", os: "web", brand: "google", browser: "google_chrome", displayName: "Chrome on Pixel 9 Pro", width: 448, height: 998, tvQuality: null },
|
|
49
|
+
{ id: "google_pixel_67_pro", type: "phone", os: "google", brand: "google", browser: null, displayName: "Google Pixel 7 Pro", width: 412, height: 892, tvQuality: null },
|
|
50
|
+
{ id: "google_pixel_5", type: "phone", os: "google", brand: "google", browser: null, displayName: "Google Pixel 5", width: 393, height: 851, tvQuality: null },
|
|
51
|
+
{ id: "google_pixel_3", type: "phone", os: "google", brand: null, browser: null, displayName: "Google Pixel 3", width: 394, height: 786, tvQuality: null },
|
|
52
|
+
{ id: "ipadair_13", type: "tablet", os: "apple", brand: null, browser: null, displayName: "iPad Air 13 2025", width: 1024, height: 1366, tvQuality: null },
|
|
53
|
+
{ id: "ipadair_11", type: "tablet", os: "apple", brand: null, browser: null, displayName: "iPad Air 11 2026", width: 820, height: 1180, tvQuality: null },
|
|
54
|
+
{ id: "ipadpro_13", type: "tablet", os: "apple", brand: null, browser: null, displayName: "iPad Pro 13 2025", width: 1024, height: 1366, tvQuality: null },
|
|
55
|
+
{ id: "ipadmini_2021", type: "tablet", os: "apple", brand: null, browser: null, displayName: "iPad Mini 2021", width: 744, height: 1059, tvQuality: null },
|
|
56
|
+
{ id: "samsung_galaxy_tab_s9", type: "tablet", os: "google", brand: "samsung", browser: null, displayName: "Samsung Galaxy Tab S9", width: 753, height: 1204, tvQuality: null },
|
|
57
|
+
{ id: "samsung_galaxy_a9plus", type: "tablet", os: "google", brand: "samsung", browser: null, displayName: "Samsung Galaxy Tab A9 Plus", width: 800, height: 1280, tvQuality: null },
|
|
58
|
+
];
|
|
59
|
+
export const PREVIEW_DEVICE_BY_ID = new Map(PREVIEW_DEVICES.map((d) => [d.id, d]));
|
package/dist/program.js
CHANGED
|
@@ -19,6 +19,7 @@ export function createProgram(h, options = {}) {
|
|
|
19
19
|
.option(HOST_FLAG, HOST_HELP)
|
|
20
20
|
.option("--app <id-or-name>", "skip the app picker")
|
|
21
21
|
.option("--platform <id-or-name>", "skip the platform picker")
|
|
22
|
+
.option("--device <id-or-name>", "skip the device picker (preset label, previewer id or name)")
|
|
22
23
|
.action(async (_opts, cmd) => {
|
|
23
24
|
const opts = cmd.optsWithGlobals();
|
|
24
25
|
process.exitCode = await h.shell(opts);
|
|
@@ -72,10 +73,20 @@ export function createProgram(h, options = {}) {
|
|
|
72
73
|
.description("Verify an App Platform ID and configure the SDK against it (headless).")
|
|
73
74
|
.requiredOption("--app-platform-id <uuid>", "the App Platform ID (nami platforms shows it)")
|
|
74
75
|
.option(HOST_FLAG, HOST_HELP)
|
|
76
|
+
.option("--platform-type <type>", "apple | google | amazon_iap | roku_pay | web — selects the device family (default preset when --device is omitted)")
|
|
77
|
+
.option("--device <id-or-name>", "target device: preset label (iPad), previewer id (ipadair_13) or name; needs --platform-type")
|
|
75
78
|
.option("--json", "machine-readable result")
|
|
76
79
|
.action(async (_opts, cmd) => {
|
|
77
80
|
process.exitCode = await h.configure(cmd.optsWithGlobals());
|
|
78
81
|
});
|
|
82
|
+
program
|
|
83
|
+
.command("devices")
|
|
84
|
+
.description("List the target device presets and the full previewer device list for a platform type.")
|
|
85
|
+
.requiredOption("--platform-type <type>", "apple | google | amazon_iap | roku_pay | web")
|
|
86
|
+
.option("--json", "machine-readable output")
|
|
87
|
+
.action(async (_opts, cmd) => {
|
|
88
|
+
process.exitCode = await h.devices(cmd.optsWithGlobals());
|
|
89
|
+
});
|
|
79
90
|
program
|
|
80
91
|
.command("config")
|
|
81
92
|
.description("Show the persisted CLI state (host, org, current app/platform, recent, onboarding). Never contains secrets.")
|
|
@@ -6,12 +6,12 @@ import { palette } from "../theme.js";
|
|
|
6
6
|
import { configureSdk } from "../sdk/configure.js";
|
|
7
7
|
import { SDK_DISABLED_MESSAGE, verifyAppPlatform } from "../sdk/verify.js";
|
|
8
8
|
/** Verify → (warn if the SDK is disabled) → configure. Every failure names the next action. */
|
|
9
|
-
export function ConfigureScreen({ host, appPlatformId, onDone, onBack, verify = verifyAppPlatform, configure = configureSdk, phase }) {
|
|
9
|
+
export function ConfigureScreen({ host, appPlatformId, device, onDone, onBack, verify = verifyAppPlatform, configure = configureSdk, phase }) {
|
|
10
10
|
const [state, setState] = useState({ kind: "verifying" });
|
|
11
11
|
const run = async (verified) => {
|
|
12
12
|
setState({ kind: "configuring" });
|
|
13
13
|
try {
|
|
14
|
-
onDone(await configure({ host, appPlatformId }, { verified }));
|
|
14
|
+
onDone(await configure({ host, appPlatformId, device }, { verified }));
|
|
15
15
|
}
|
|
16
16
|
catch (error) {
|
|
17
17
|
setState({ kind: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { SelectList } from "../components/SelectList.js";
|
|
4
|
+
import { deviceById, devicesFor, presetsFor } from "../devices/presets.js";
|
|
5
|
+
const MORE = "__more__";
|
|
6
|
+
const size = (d) => `${d.width}×${d.height}`;
|
|
7
|
+
/**
|
|
8
|
+
* Tier 1: the platform's coarse presets (iPhone / iPad / Apple TV …).
|
|
9
|
+
* Tier 2 ("More devices…"): every previewer device that fits the platform.
|
|
10
|
+
*/
|
|
11
|
+
export function PickDevice({ platformType, platformName, currentDeviceId, onSelect, onCancel }) {
|
|
12
|
+
const [mode, setMode] = useState("presets");
|
|
13
|
+
const all = devicesFor(platformType);
|
|
14
|
+
if (mode === "all") {
|
|
15
|
+
return (_jsx(SelectList, { title: `Devices for ${platformName}`, items: all.map((d) => ({ key: d.id, label: d.displayName, badge: d.type, hint: size(d) })), initialKey: currentDeviceId, onSelect: (key) => { const d = deviceById(key); if (d)
|
|
16
|
+
onSelect(d); }, onCancel: () => setMode("presets"), footer: "\u2191/\u2193 move \u00B7 Enter select \u00B7 type to filter \u00B7 Esc back to presets" }, "all"));
|
|
17
|
+
}
|
|
18
|
+
const presets = presetsFor(platformType);
|
|
19
|
+
return (_jsx(SelectList, { title: `Target device for ${platformName}`, items: [
|
|
20
|
+
...presets.map((p) => { const d = deviceById(p.deviceId); return { key: p.deviceId, label: p.label, hint: d ? `${d.displayName} · ${size(d)}` : undefined }; }),
|
|
21
|
+
{ key: MORE, label: "More devices…", hint: `${all.length} previewer devices` },
|
|
22
|
+
], initialKey: currentDeviceId && presets.some((p) => p.deviceId === currentDeviceId) ? currentDeviceId : undefined, onSelect: (key) => {
|
|
23
|
+
if (key === MORE)
|
|
24
|
+
return setMode("all");
|
|
25
|
+
const d = deviceById(key);
|
|
26
|
+
if (d)
|
|
27
|
+
onSelect(d);
|
|
28
|
+
}, onCancel: onCancel }, "presets"));
|
|
29
|
+
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { palette } from "../theme.js";
|
|
4
4
|
/** One-line context bar: `org / app / platform (type)`. Always visible in the shell. */
|
|
5
|
-
export function SelectionHeader({ orgName, appName, platformName, platformType, host }) {
|
|
5
|
+
export function SelectionHeader({ orgName, appName, platformName, platformType, deviceName, host }) {
|
|
6
6
|
const parts = [orgName, appName, platformName ? `${platformName}${platformType ? ` (${platformType})` : ""}` : null].filter(Boolean);
|
|
7
|
-
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: palette.aqua, children: "nami" }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { children: parts.join(" / ") || "not signed in" }), host && host !== "production" && _jsxs(Text, { dimColor: true, children: [" [", host, "]"] })] }));
|
|
7
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: palette.aqua, children: "nami" }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { children: parts.join(" / ") || "not signed in" }), deviceName && _jsxs(Text, { dimColor: true, children: [" \u00B7 ", deviceName] }), host && host !== "production" && _jsxs(Text, { dimColor: true, children: [" [", host, "]"] })] }));
|
|
8
8
|
}
|
package/dist/screens/Shell.js
CHANGED
|
@@ -4,6 +4,8 @@ import { Box, Text, useApp, useInput } from "ink";
|
|
|
4
4
|
import { LoginFlow } from "./LoginFlow.js";
|
|
5
5
|
import { PickApp } from "./PickApp.js";
|
|
6
6
|
import { PickPlatform } from "./PickPlatform.js";
|
|
7
|
+
import { PickDevice } from "./PickDevice.js";
|
|
8
|
+
import { deviceById, presetsFor, resolveDevice, toSelection } from "../devices/presets.js";
|
|
7
9
|
import { SelectionHeader } from "./SelectionHeader.js";
|
|
8
10
|
import { Thinking } from "../components/Thinking.js";
|
|
9
11
|
import { palette } from "../theme.js";
|
|
@@ -18,13 +20,13 @@ import { appFrom, platformFrom, selectionFrom } from "../config/selection.js";
|
|
|
18
20
|
* Selection is in-memory for now (persistence: NAM-4501; configure: NAM-4500;
|
|
19
21
|
* full Switch with Recent/org switching: NAM-4503).
|
|
20
22
|
*/
|
|
21
|
-
export function Shell({ ctx, appQuery, platformQuery, onExit, exitWhenDone = true, phase, verify, configure }) {
|
|
23
|
+
export function Shell({ ctx, appQuery, platformQuery, deviceQuery, onExit, exitWhenDone = true, phase, verify, configure }) {
|
|
22
24
|
const { exit } = useApp();
|
|
23
25
|
const [state, setState] = useState({ kind: "boot" });
|
|
24
26
|
const [orgName, setOrgName] = useState(null);
|
|
25
27
|
const [orgId, setOrgId] = useState(null);
|
|
26
28
|
const step = (name) => void ctx.config.setOnboardingStep(name).catch(() => { });
|
|
27
|
-
const [queries, setQueries] = useState({ app: appQuery, platform: platformQuery });
|
|
29
|
+
const [queries, setQueries] = useState({ app: appQuery, platform: platformQuery, device: deviceQuery });
|
|
28
30
|
const finish = (code) => {
|
|
29
31
|
onExit?.(code);
|
|
30
32
|
if (exitWhenDone)
|
|
@@ -38,6 +40,27 @@ export function Shell({ ctx, appQuery, platformQuery, onExit, exitWhenDone = tru
|
|
|
38
40
|
return fatal(error.message, 2);
|
|
39
41
|
fatal(error instanceof Error ? error.message : String(error));
|
|
40
42
|
};
|
|
43
|
+
/** After a platform is chosen: --device → resolve; single preset (Roku, Fire TV) → auto; else ask. */
|
|
44
|
+
const chooseDevice = (app, platform, current) => {
|
|
45
|
+
if (queries.device) {
|
|
46
|
+
try {
|
|
47
|
+
const device = resolveDevice(platform.type, queries.device);
|
|
48
|
+
setQueries((q) => ({ ...q, device: undefined }));
|
|
49
|
+
step("configure");
|
|
50
|
+
return setState({ kind: "configure", app, platform, device });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return handleError(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const presets = presetsFor(platform.type);
|
|
57
|
+
if (presets.length <= 1) {
|
|
58
|
+
const device = presets[0] ? deviceById(presets[0].deviceId) ?? null : null;
|
|
59
|
+
step("configure");
|
|
60
|
+
return setState({ kind: "configure", app, platform, device });
|
|
61
|
+
}
|
|
62
|
+
setState({ kind: "pick-device", app, platform, current });
|
|
63
|
+
};
|
|
41
64
|
const loadApps = async (current) => {
|
|
42
65
|
setState({ kind: "busy", label: "Loading your apps…" });
|
|
43
66
|
try {
|
|
@@ -61,7 +84,7 @@ export function Shell({ ctx, appQuery, platformQuery, onExit, exitWhenDone = tru
|
|
|
61
84
|
if (queries.platform) {
|
|
62
85
|
const platform = findPlatform(platforms, queries.platform);
|
|
63
86
|
setQueries((q) => ({ ...q, platform: undefined }));
|
|
64
|
-
return
|
|
87
|
+
return chooseDevice(app, platform);
|
|
65
88
|
}
|
|
66
89
|
step("pick-platform");
|
|
67
90
|
setState({ kind: "pick-platform", app, platforms, current });
|
|
@@ -83,8 +106,12 @@ export function Shell({ ctx, appQuery, platformQuery, onExit, exitWhenDone = tru
|
|
|
83
106
|
const { config } = await ctx.config.load();
|
|
84
107
|
// Zero-prompt re-run: a persisted selection (and no --app/--platform override) goes straight to configure.
|
|
85
108
|
if (config.current && !queries.app && !queries.platform) {
|
|
109
|
+
const platform = platformFrom(config.current);
|
|
110
|
+
const stored = config.current.device ? deviceById(config.current.device.id) : undefined;
|
|
111
|
+
const fallback = presetsFor(platform.type)[0];
|
|
112
|
+
const device = stored ?? (fallback ? deviceById(fallback.deviceId) ?? null : null);
|
|
86
113
|
step("configure");
|
|
87
|
-
return setState({ kind: "configure", app: appFrom(config.current), platform
|
|
114
|
+
return setState({ kind: "configure", app: appFrom(config.current), platform, device });
|
|
88
115
|
}
|
|
89
116
|
await loadApps();
|
|
90
117
|
})();
|
|
@@ -102,13 +129,15 @@ export function Shell({ ctx, appQuery, platformQuery, onExit, exitWhenDone = tru
|
|
|
102
129
|
return void loadApps(state.app);
|
|
103
130
|
if (input === "p")
|
|
104
131
|
return void loadPlatforms(state.app, state.platform);
|
|
132
|
+
if (input === "d" && presetsFor(state.platform.type).length > 1)
|
|
133
|
+
return setState({ kind: "pick-device", app: state.app, platform: state.platform, current: state.device ?? undefined });
|
|
105
134
|
}, { isActive: state.kind === "home" });
|
|
106
|
-
const header = (_jsx(SelectionHeader, { orgName: orgName, appName: "app" in state ? state.app.name : undefined, platformName: "platform" in state ? state.platform.name : undefined, platformType: "platform" in state ? state.platform.typeLabel : undefined, host: ctx.host.name }));
|
|
107
|
-
return (_jsxs(Box, { flexDirection: "column", children: [header, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.kind === "boot" && _jsx(Thinking, { label: "Starting\u2026", phase: phase }), state.kind === "signin" && (_jsxs(Box, { flexDirection: "column", children: [state.notice && _jsx(Text, { color: palette.aqua, children: state.notice }), _jsx(LoginFlow, { auth: ctx.auth, store: ctx.store, host: ctx.host, exitWhenDone: false, phase: phase, onDone: (session) => { setOrgName(session.orgName); setOrgId(session.orgId); void loadApps(); }, onFail: (e) => fatal(e.message) })] })), state.kind === "busy" && _jsx(Thinking, { label: state.label, phase: phase }), state.kind === "pick-app" && (_jsx(PickApp, { apps: state.apps, orgName: orgName, baseUrl: ctx.host.baseUrl, currentAppId: state.current?.id, onSelect: (app) => void loadPlatforms(app), onCancel: () => finish(0) })), state.kind === "pick-platform" && (_jsx(PickPlatform, { appName: state.app.name, platforms: state.platforms, currentPlatformId: state.current?.id, onSelect: (platform) => { step("configure"); setState({ kind: "configure", app: state.app, platform }); }, onCancel: () => void
|
|
135
|
+
const header = (_jsx(SelectionHeader, { orgName: orgName, appName: "app" in state ? state.app.name : undefined, platformName: "platform" in state ? state.platform.name : undefined, platformType: "platform" in state ? state.platform.typeLabel : undefined, deviceName: "device" in state ? state.device?.displayName : undefined, host: ctx.host.name }));
|
|
136
|
+
return (_jsxs(Box, { flexDirection: "column", children: [header, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.kind === "boot" && _jsx(Thinking, { label: "Starting\u2026", phase: phase }), state.kind === "signin" && (_jsxs(Box, { flexDirection: "column", children: [state.notice && _jsx(Text, { color: palette.aqua, children: state.notice }), _jsx(LoginFlow, { auth: ctx.auth, store: ctx.store, host: ctx.host, exitWhenDone: false, phase: phase, onDone: (session) => { setOrgName(session.orgName); setOrgId(session.orgId); void loadApps(); }, onFail: (e) => fatal(e.message) })] })), state.kind === "busy" && _jsx(Thinking, { label: state.label, phase: phase }), state.kind === "pick-app" && (_jsx(PickApp, { apps: state.apps, orgName: orgName, baseUrl: ctx.host.baseUrl, currentAppId: state.current?.id, onSelect: (app) => void loadPlatforms(app), onCancel: () => finish(0) })), state.kind === "pick-platform" && (_jsx(PickPlatform, { appName: state.app.name, platforms: state.platforms, currentPlatformId: state.current?.id, onSelect: (platform) => chooseDevice(state.app, platform), onCancel: () => void loadApps(state.app) })), state.kind === "pick-device" && (_jsx(PickDevice, { platformType: state.platform.type, platformName: state.platform.name, currentDeviceId: state.current?.id, onSelect: (device) => { step("configure"); setState({ kind: "configure", app: state.app, platform: state.platform, device }); }, onCancel: () => void loadPlatforms(state.app, state.platform) })), state.kind === "configure" && (_jsx(ConfigureScreen, { host: ctx.host, appPlatformId: state.platform.id, device: state.device ? { platformType: state.platform.type, id: state.device.id } : undefined, phase: phase, verify: verify, configure: configure, onDone: (result) => {
|
|
108
137
|
void ctx.config
|
|
109
|
-
.setCurrent(selectionFrom(state.app, state.platform), { host: ctx.host.name, session: { orgId, orgName } })
|
|
138
|
+
.setCurrent(selectionFrom(state.app, state.platform, state.device ? toSelection(state.device) : null), { host: ctx.host.name, session: { orgId, orgName } })
|
|
110
139
|
.then(() => ctx.config.markOnboardingDone())
|
|
111
140
|
.catch(() => { });
|
|
112
|
-
setState({ kind: "home", app: state.app, platform: state.platform, result });
|
|
113
|
-
}, onBack: () =>
|
|
141
|
+
setState({ kind: "home", app: state.app, platform: state.platform, device: state.device, result });
|
|
142
|
+
}, onBack: () => chooseDevice(state.app, state.platform, state.device ?? undefined) })), state.kind === "home" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Text, { bold: true, children: [state.app.name, " \u00B7 ", state.platform.name, " ", _jsxs(Text, { color: palette.secondaryBlue, children: ["[", state.platform.typeLabel, "]"] })] }), _jsxs(Text, { children: ["App Platform ID: ", _jsx(Text, { color: palette.aqua, children: state.platform.id })] }), state.device && _jsxs(Text, { children: ["Device: ", state.device.displayName, " ", _jsxs(Text, { dimColor: true, children: ["(", state.device.type, ", ", state.device.width, "\u00D7", state.device.height, ")"] })] }), _jsxs(Text, { children: [state.result.sdkEnabled ? "SDK enabled" : "SDK disabled", " \u00B7 ", state.result.configureState, state.result.deviceId ? ` · device ${state.result.deviceId}` : ""] }), _jsxs(Text, { children: ["placements ", _jsx(Text, { bold: true, children: state.result.counts.placements }), " \u00B7 products ", _jsx(Text, { bold: true, children: state.result.counts.products }), " \u00B7 paywalls ", _jsx(Text, { bold: true, children: state.result.counts.paywalls })] }), _jsx(Text, { dimColor: true, children: "Launch a placement lands in NAM-4502." })] }), _jsxs(Text, { dimColor: true, children: ["s switch app \u00B7 p switch platform", presetsFor(state.platform.type).length > 1 ? " · d switch device" : "", " \u00B7 q quit"] })] })), state.kind === "fatal" && _jsx(Text, { color: palette.coral, children: state.message })] })] }));
|
|
114
143
|
}
|
package/dist/sdk/configure.js
CHANGED
|
@@ -3,6 +3,7 @@ import { registerTerminalAdapters } from "../adapters/index.js";
|
|
|
3
3
|
import { FileStorageAdapter, storagePathFor } from "../adapters/file-storage.adapter.js";
|
|
4
4
|
import { configDir } from "../paths.js";
|
|
5
5
|
import { verifyAppPlatform } from "./verify.js";
|
|
6
|
+
import { deviceById, presetsFor, resolveDevice, toSelection } from "../devices/presets.js";
|
|
6
7
|
let activeStorage = null;
|
|
7
8
|
let configuredOnce = false;
|
|
8
9
|
/** The storage adapter registered by the last configureSdk() call (tests/inspection). */
|
|
@@ -15,12 +16,13 @@ export const currentStorage = () => activeStorage;
|
|
|
15
16
|
export async function configureSdk(input, deps = {}) {
|
|
16
17
|
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
17
18
|
const verified = deps.verified ?? (await verifyAppPlatform(input.host.baseUrl, input.appPlatformId, fetchImpl));
|
|
18
|
-
const
|
|
19
|
+
const preset = resolvePreset(input.device);
|
|
20
|
+
const storagePath = storagePathFor(input.appPlatformId, deps.configDir ?? configDir(), preset?.device.id);
|
|
19
21
|
if (configuredOnce)
|
|
20
22
|
await Nami.reset();
|
|
21
23
|
activeStorage?.dispose();
|
|
22
24
|
activeStorage = new FileStorageAdapter(storagePath);
|
|
23
|
-
registerTerminalAdapters({ storage: activeStorage });
|
|
25
|
+
registerTerminalAdapters({ storage: activeStorage, device: preset ?? undefined });
|
|
24
26
|
const namiCommands = input.host.name === "staging" ? ["useStagingAPI"] : [];
|
|
25
27
|
const state = await Nami.configure({ appPlatformID: input.appPlatformId, namiCommands, logLevel: input.logLevel ?? "warn" });
|
|
26
28
|
configuredOnce = true;
|
|
@@ -38,8 +40,20 @@ export async function configureSdk(input, deps = {}) {
|
|
|
38
40
|
},
|
|
39
41
|
deviceId: safeDeviceId(),
|
|
40
42
|
storagePath,
|
|
43
|
+
device: preset ? toSelection(preset.device) : null,
|
|
41
44
|
};
|
|
42
45
|
}
|
|
46
|
+
/** Pick the preset: explicit id/label/name, else the platform's first preset (phone or TV). */
|
|
47
|
+
function resolvePreset(input) {
|
|
48
|
+
if (!input)
|
|
49
|
+
return null;
|
|
50
|
+
const { platformType, id } = input;
|
|
51
|
+
if (id)
|
|
52
|
+
return { device: resolveDevice(platformType, id), platformType };
|
|
53
|
+
const first = presetsFor(platformType)[0];
|
|
54
|
+
const device = first ? deviceById(first.deviceId) : undefined;
|
|
55
|
+
return device ? { device, platformType } : null;
|
|
56
|
+
}
|
|
43
57
|
function safeCount(read) {
|
|
44
58
|
try {
|
|
45
59
|
return read();
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** CLI package version. Stamped by scripts/version.sh from the repo VERSION file — do not hand-edit. */
|
|
2
|
-
export const NAMI_CLI_VERSION = "3.4.11-dev.
|
|
2
|
+
export const NAMI_CLI_VERSION = "3.4.11-dev.202609222222";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@namiml/cli",
|
|
3
|
-
"version": "3.4.11-dev.
|
|
3
|
+
"version": "3.4.11-dev.202609222222",
|
|
4
4
|
"description": "Nami SDK developer CLI — terminal test app, implementation and flow checks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nami",
|
|
@@ -27,14 +27,15 @@
|
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/namiml/nami-
|
|
30
|
+
"url": "https://github.com/namiml/nami-releases.git",
|
|
31
|
+
"directory": "cli"
|
|
31
32
|
},
|
|
32
33
|
"homepage": "https://www.namiml.com",
|
|
33
34
|
"publishConfig": {
|
|
34
35
|
"access": "public"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@namiml/sdk-core": "3.4.11-dev.
|
|
38
|
+
"@namiml/sdk-core": "3.4.11-dev.202609222222",
|
|
38
39
|
"commander": "^15",
|
|
39
40
|
"env-paths": "^4",
|
|
40
41
|
"ink": "^6.0.0",
|