@namiml/cli 3.4.11-dev.202609230009 → 3.4.11-dev.202609231629
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/dist/cli.js +2 -0
- package/dist/commands/configure.js +3 -2
- package/dist/commands/onboarding.js +12 -2
- package/dist/commands/placements.js +56 -0
- package/dist/config/sdkconfig.js +69 -0
- package/dist/events/bus.js +58 -0
- package/dist/onboarding/run.js +146 -0
- package/dist/placements/launch.js +160 -0
- package/dist/placements/list.js +52 -0
- package/dist/program.js +17 -1
- package/dist/render/action-map.js +101 -0
- package/dist/render/composite.js +161 -0
- package/dist/screens/EventsPane.js +135 -0
- package/dist/screens/LaunchScreen.js +74 -0
- package/dist/screens/LoginFlow.js +1 -1
- package/dist/screens/NewToNami.js +27 -0
- package/dist/screens/PickOrg.js +2 -2
- package/dist/screens/PickPlacement.js +11 -0
- package/dist/screens/Shell.js +126 -13
- package/dist/screens/SuccessScreen.js +2 -2
- package/dist/screens/WelcomeScreen.js +15 -0
- package/dist/sdk/summary.js +24 -2
- package/dist/utils/open-url.js +33 -0
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/screens/Shell.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
|
-
import { Box, Text, useApp, useInput } from "ink";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { LoginFlow } from "./LoginFlow.js";
|
|
5
5
|
import { PickApp } from "./PickApp.js";
|
|
6
6
|
import { PickPlatform } from "./PickPlatform.js";
|
|
7
7
|
import { PickDevice } from "./PickDevice.js";
|
|
8
8
|
import { PickOrg } from "./PickOrg.js";
|
|
9
9
|
import { SwitchScreen } from "./SwitchScreen.js";
|
|
10
|
+
import { WelcomeScreen } from "./WelcomeScreen.js";
|
|
11
|
+
import { NewToNami } from "./NewToNami.js";
|
|
10
12
|
import { SuccessScreen } from "./SuccessScreen.js";
|
|
13
|
+
import { PickPlacement } from "./PickPlacement.js";
|
|
14
|
+
import { LaunchScreen, focusableNodes } from "./LaunchScreen.js";
|
|
15
|
+
import { corePlacements } from "../placements/list.js";
|
|
16
|
+
import { compositeForPlacement, pressNode, startLaunch as startLaunchFn } from "../placements/launch.js";
|
|
17
|
+
import { EventBus } from "../events/bus.js";
|
|
18
|
+
import { terminalColumns } from "../utils/terminal.js";
|
|
11
19
|
import { deviceById, presetsFor, resolveDevice, toSelection } from "../devices/presets.js";
|
|
12
20
|
import { SelectionHeader } from "./SelectionHeader.js";
|
|
13
21
|
import { Thinking } from "../components/Thinking.js";
|
|
@@ -19,6 +27,7 @@ import { findApp, findPlatform, ResolveError } from "../control-center/resolve.j
|
|
|
19
27
|
import { findAppPlatformById, pastedStandIn } from "../control-center/lookup.js";
|
|
20
28
|
import { ConfigureScreen } from "./ConfigureScreen.js";
|
|
21
29
|
import { appFrom, platformFrom, selectionFrom } from "../config/selection.js";
|
|
30
|
+
import { resolveHost } from "../hosts.js";
|
|
22
31
|
import { verifyAppPlatform as verifyAppPlatformFn, VerifyError } from "../sdk/verify.js";
|
|
23
32
|
import TextInput from "ink-text-input";
|
|
24
33
|
/**
|
|
@@ -28,14 +37,27 @@ import TextInput from "ink-text-input";
|
|
|
28
37
|
* pasted App Platform ID. Every path funnels back through `configure`, so the
|
|
29
38
|
* SDK is re-configured in place and each platform keeps its own storage file.
|
|
30
39
|
*/
|
|
31
|
-
export function Shell({ ctx, appQuery, platformQuery, deviceQuery, onExit, exitWhenDone = true, phase, verify, configure }) {
|
|
40
|
+
export function Shell({ ctx, appQuery, platformQuery, deviceQuery, onExit, exitWhenDone = true, phase, hostWasExplicit = false, verify, configure, placementsSource, compositeSource, startLaunch }) {
|
|
32
41
|
const { exit } = useApp();
|
|
42
|
+
const { stdout } = useStdout();
|
|
33
43
|
const [state, setState] = useState({ kind: "boot" });
|
|
34
44
|
const [orgName, setOrgName] = useState(null);
|
|
35
45
|
const [orgId, setOrgId] = useState(null);
|
|
36
46
|
const step = (name) => void ctx.config.setOnboardingStep(name).catch(() => { });
|
|
37
47
|
const [queries, setQueries] = useState({ app: appQuery, platform: platformQuery, device: deviceQuery });
|
|
38
48
|
const [pasted, setPasted] = useState("");
|
|
49
|
+
// The paste route may retry on another host after a 404, without re-typing the id.
|
|
50
|
+
const [pasteHost, setPasteHost] = useState(ctx.host);
|
|
51
|
+
// One bus for the whole session. A launch clears it, so a run always starts
|
|
52
|
+
// empty, but `seq` keeps climbing — it identifies events, it is not a count.
|
|
53
|
+
const [bus] = useState(() => new EventBus());
|
|
54
|
+
const [events, setEvents] = useState([]);
|
|
55
|
+
const [paused, setPaused] = useState(false);
|
|
56
|
+
const [expanded, setExpanded] = useState(false);
|
|
57
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
58
|
+
const [notice, setNotice] = useState(undefined);
|
|
59
|
+
// The bus subscriber runs outside React's render, so pause is read from a ref.
|
|
60
|
+
const pausedRef = useRef(false);
|
|
39
61
|
const finish = (code) => {
|
|
40
62
|
onExit?.(code);
|
|
41
63
|
if (exitWhenDone)
|
|
@@ -120,22 +142,99 @@ export function Shell({ ctx, appQuery, platformQuery, deviceQuery, onExit, exitW
|
|
|
120
142
|
* its name by scanning the apps this account can see. An id from another org
|
|
121
143
|
* still works — it just shows as a pasted id until the device step.
|
|
122
144
|
*/
|
|
123
|
-
const usePastedId = async (home, raw) => {
|
|
145
|
+
const usePastedId = async (home, raw, host = pasteHost) => {
|
|
124
146
|
const id = raw.trim();
|
|
125
147
|
if (!id)
|
|
126
148
|
return;
|
|
149
|
+
setPasteHost(host);
|
|
127
150
|
setState({ kind: "busy", label: "Checking that ID…" });
|
|
128
151
|
try {
|
|
129
|
-
await (verify ?? verifyAppPlatformFn)(
|
|
152
|
+
await (verify ?? verifyAppPlatformFn)(host.baseUrl, id);
|
|
130
153
|
}
|
|
131
154
|
catch (error) {
|
|
132
|
-
if (error instanceof VerifyError)
|
|
133
|
-
|
|
155
|
+
if (error instanceof VerifyError) {
|
|
156
|
+
// A first-run id pasted against production is most often a staging id.
|
|
157
|
+
const offer = error.code === "not_found" && host.name === "production" && !hostWasExplicit ? id : undefined;
|
|
158
|
+
const notice = offer ? `${error.message}\nPress s to try staging instead.` : error.message;
|
|
159
|
+
return setState({ kind: "paste-id", home, notice, offerStaging: offer });
|
|
160
|
+
}
|
|
134
161
|
return handleError(error);
|
|
135
162
|
}
|
|
136
163
|
const named = (await findAppPlatformById(ctx.session, id)) ?? pastedStandIn(id);
|
|
137
164
|
chooseDevice(named.app, named.platform);
|
|
138
165
|
};
|
|
166
|
+
/** `l`: the placements this configured platform can launch. */
|
|
167
|
+
const openPlacements = (home) => {
|
|
168
|
+
try {
|
|
169
|
+
setState({ kind: "pick-placement", home, placements: (placementsSource ?? corePlacements)() });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
handleError(error);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/** Enter on a placement: reset the pane, outline the page, then subscribe and launch. */
|
|
176
|
+
const openLaunch = (home, placement) => {
|
|
177
|
+
bus.clear();
|
|
178
|
+
setEvents([]);
|
|
179
|
+
setFocusIndex(0);
|
|
180
|
+
setExpanded(false);
|
|
181
|
+
setNotice(undefined);
|
|
182
|
+
pausedRef.current = false;
|
|
183
|
+
setPaused(false);
|
|
184
|
+
setState({ kind: "launch", home, placement, composite: (compositeSource ?? compositeForPlacement)(placement.label) });
|
|
185
|
+
};
|
|
186
|
+
// Subscriptions live and die with the launch state, so leaving the screen
|
|
187
|
+
// unregisters every handler it added.
|
|
188
|
+
const launchLabel = state.kind === "launch" ? state.placement.label : null;
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (launchLabel === null)
|
|
191
|
+
return;
|
|
192
|
+
let live = true;
|
|
193
|
+
let session;
|
|
194
|
+
const unsubscribe = bus.subscribe(() => {
|
|
195
|
+
if (!pausedRef.current)
|
|
196
|
+
setEvents(bus.all());
|
|
197
|
+
});
|
|
198
|
+
void (startLaunch ?? startLaunchFn)(launchLabel, bus)
|
|
199
|
+
.then((started) => {
|
|
200
|
+
if (live)
|
|
201
|
+
session = started;
|
|
202
|
+
else
|
|
203
|
+
started.dispose();
|
|
204
|
+
})
|
|
205
|
+
.catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
|
|
206
|
+
return () => {
|
|
207
|
+
live = false;
|
|
208
|
+
unsubscribe();
|
|
209
|
+
session?.dispose();
|
|
210
|
+
};
|
|
211
|
+
}, [launchLabel]);
|
|
212
|
+
/** Press: report the mapped action, or say why this control reports nothing. */
|
|
213
|
+
const press = (node) => {
|
|
214
|
+
setNotice(pressNode(node).reason);
|
|
215
|
+
};
|
|
216
|
+
const togglePause = () => {
|
|
217
|
+
const next = !pausedRef.current;
|
|
218
|
+
pausedRef.current = next;
|
|
219
|
+
setPaused(next);
|
|
220
|
+
// Resuming catches up on everything that arrived while paused.
|
|
221
|
+
if (!next)
|
|
222
|
+
setEvents(bus.all());
|
|
223
|
+
};
|
|
224
|
+
const clearEvents = () => {
|
|
225
|
+
bus.clear();
|
|
226
|
+
setEvents([]);
|
|
227
|
+
};
|
|
228
|
+
const onWelcomeChoice = (choice) => {
|
|
229
|
+
if (choice === "signin") {
|
|
230
|
+
step("signin");
|
|
231
|
+
return setState({ kind: "signin" });
|
|
232
|
+
}
|
|
233
|
+
if (choice === "new")
|
|
234
|
+
return setState({ kind: "new-to-nami" });
|
|
235
|
+
step("configure");
|
|
236
|
+
setState({ kind: "paste-id", home: null });
|
|
237
|
+
};
|
|
139
238
|
const onSwitchChoice = (home, action) => {
|
|
140
239
|
if (action.kind === "recent")
|
|
141
240
|
return useRecent(home, action.selection);
|
|
@@ -187,11 +286,18 @@ export function Shell({ ctx, appQuery, platformQuery, deviceQuery, onExit, exitW
|
|
|
187
286
|
setOrgName(stored?.orgName ?? null);
|
|
188
287
|
setOrgId(stored?.orgId ?? null);
|
|
189
288
|
const signedIn = Boolean(stored || ctx.env.NAMI_TOKEN);
|
|
289
|
+
const { config } = await ctx.config.load();
|
|
290
|
+
const asked = Boolean(queries.app || queries.platform || queries.device);
|
|
291
|
+
// First run only: the step is still `welcome` and nothing has been chosen.
|
|
292
|
+
// Any later step resumes where it left off, so quitting mid-onboarding
|
|
293
|
+
// never re-greets you.
|
|
294
|
+
if (config.onboarding.step === "welcome" && !config.current && !asked) {
|
|
295
|
+
return setState({ kind: "welcome" });
|
|
296
|
+
}
|
|
190
297
|
if (!signedIn) {
|
|
191
298
|
step("signin");
|
|
192
299
|
return setState({ kind: "signin" });
|
|
193
300
|
}
|
|
194
|
-
const { config } = await ctx.config.load();
|
|
195
301
|
// Zero-prompt re-run: a persisted selection (and no --app/--platform override) goes straight to configure.
|
|
196
302
|
if (config.current && !queries.app && !queries.platform) {
|
|
197
303
|
const platform = platformFrom(config.current);
|
|
@@ -208,12 +314,16 @@ export function Shell({ ctx, appQuery, platformQuery, deviceQuery, onExit, exitW
|
|
|
208
314
|
if (state.kind === "fatal")
|
|
209
315
|
finish(state.code);
|
|
210
316
|
}, [state.kind]);
|
|
211
|
-
useInput((
|
|
317
|
+
useInput((input, key) => {
|
|
212
318
|
if (state.kind !== "paste-id")
|
|
213
319
|
return;
|
|
320
|
+
if (state.offerStaging && input === "s") {
|
|
321
|
+
const id = state.offerStaging;
|
|
322
|
+
return void usePastedId(state.home, id, resolveHost("staging"));
|
|
323
|
+
}
|
|
214
324
|
if (key.escape) {
|
|
215
325
|
setPasted("");
|
|
216
|
-
setState(state.home);
|
|
326
|
+
setState(state.home ?? { kind: "welcome" });
|
|
217
327
|
}
|
|
218
328
|
}, { isActive: state.kind === "paste-id" });
|
|
219
329
|
useInput((input, key) => {
|
|
@@ -228,11 +338,11 @@ export function Shell({ ctx, appQuery, platformQuery, deviceQuery, onExit, exitW
|
|
|
228
338
|
if (input === "d" && presetsFor(state.platform.type).length > 1)
|
|
229
339
|
return setState({ kind: "pick-device", app: state.app, platform: state.platform, current: state.device ?? undefined });
|
|
230
340
|
if (input === "l")
|
|
231
|
-
return
|
|
341
|
+
return openPlacements(state);
|
|
232
342
|
}, { isActive: state.kind === "home" });
|
|
233
343
|
// The hub and its sub-screens keep the home state they came from, so the header
|
|
234
344
|
// keeps showing what is loaded while you choose what to switch to.
|
|
235
|
-
const shown = "home" in state ? state.home : "app" in state ? state : {};
|
|
345
|
+
const shown = ("home" in state ? state.home : "app" in state ? state : null) ?? {};
|
|
236
346
|
const header = (_jsx(SelectionHeader, { orgName: orgName, appName: shown.app?.name, platformName: shown.platform?.name, platformType: shown.platform?.typeLabel, deviceName: shown.device?.displayName, host: ctx.host.name }));
|
|
237
347
|
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) => {
|
|
238
348
|
void ctx.config
|
|
@@ -240,5 +350,8 @@ export function Shell({ ctx, appQuery, platformQuery, deviceQuery, onExit, exitW
|
|
|
240
350
|
.then(() => ctx.config.markOnboardingDone())
|
|
241
351
|
.catch(() => { });
|
|
242
352
|
setState({ kind: "home", app: state.app, platform: state.platform, device: state.device, result });
|
|
243
|
-
}, onBack: () => chooseDevice(state.app, state.platform, state.device ?? undefined) })), state.kind === "home" && (_jsx(SuccessScreen, { app: state.app, platform: state.platform, device: state.device, result: state.result, host: ctx.host, canSwitchDevice: presetsFor(state.platform.type).length > 1
|
|
353
|
+
}, onBack: () => chooseDevice(state.app, state.platform, state.device ?? undefined) })), state.kind === "home" && (_jsx(SuccessScreen, { app: state.app, platform: state.platform, device: state.device, result: state.result, host: ctx.host, canSwitchDevice: presetsFor(state.platform.type).length > 1 })), state.kind === "pick-placement" && (_jsx(PickPlacement, { placements: state.placements, onSelect: (placement) => openLaunch(state.home, placement), onCancel: () => setState(state.home) })), state.kind === "launch" && (_jsx(LaunchScreen, { placement: state.placement, composite: state.composite, deviceName: state.home.device?.displayName, events: events, width: Math.max(terminalColumns(stdout) - 2, 40), paused: paused, expanded: expanded, selectedSeq: events.at(-1)?.seq, focusIndex: focusIndex, onPress: press, onMove: (delta) => {
|
|
354
|
+
const count = focusableNodes(state.composite).length;
|
|
355
|
+
setFocusIndex((index) => (count === 0 ? 0 : (index + delta + count) % count));
|
|
356
|
+
}, onTogglePause: togglePause, onClear: clearEvents, onToggleExpand: () => setExpanded((on) => !on), onBack: () => setState(state.home), notice: notice })), state.kind === "welcome" && (_jsx(WelcomeScreen, { host: ctx.host, onChoose: onWelcomeChoice, onCancel: () => finish(0) })), state.kind === "new-to-nami" && (_jsx(NewToNami, { host: ctx.host, onContinue: () => { step("configure"); setState({ kind: "paste-id", home: null }); }, onBack: () => setState({ kind: "welcome" }) })), state.kind === "switch" && (_jsx(SwitchScreen, { recent: state.recent, current: state.current, orgName: orgName, canSwitchDevice: presetsFor(state.home.platform.type).length > 1, canSwitchOrg: true, onChoose: (action) => onSwitchChoice(state.home, action), onCancel: () => setState(state.home) })), state.kind === "pick-org" && (_jsx(PickOrg, { orgs: state.orgs, currentOrgId: orgId, onSelect: (org) => void switchOrg(org), onCancel: () => setState(state.home) })), state.kind === "paste-id" && (_jsxs(Box, { flexDirection: "column", children: [state.notice && _jsx(Text, { color: palette.coral, children: state.notice }), _jsxs(Box, { children: [_jsx(Text, { children: "App Platform ID: " }), _jsx(TextInput, { value: pasted, onChange: setPasted, onSubmit: (v) => { setPasted(""); void usePastedId(state.home, v); } })] }), _jsxs(Text, { dimColor: true, children: ["Checking ", pasteHost.name, ". nami platforms --app <app> lists them. Esc to go back."] })] })), state.kind === "fatal" && _jsx(Text, { color: palette.coral, children: state.message })] })] }));
|
|
244
357
|
}
|
|
@@ -3,9 +3,9 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { palette } from "../theme.js";
|
|
4
4
|
export const PLACEMENT_LIST_MAX = 8;
|
|
5
5
|
/** The payoff after onboarding: what the SDK found, and what to do next. */
|
|
6
|
-
export function SuccessScreen({ app, platform, device, result, host, canSwitchDevice
|
|
6
|
+
export function SuccessScreen({ app, platform, device, result, host, canSwitchDevice }) {
|
|
7
7
|
const { counts, placements } = result;
|
|
8
8
|
const shown = placements.slice(0, PLACEMENT_LIST_MAX);
|
|
9
9
|
const more = placements.length - shown.length;
|
|
10
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: palette.aqua, paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: palette.aqua, children: "Connected" }), " \u00B7 ", app.name, " \u00B7 ", platform.name, " ", _jsxs(Text, { color: palette.secondaryBlue, children: ["[", platform.typeLabel, "]"] }), device && _jsxs(Text, { children: [" \u00B7 ", device.displayName, " ", _jsxs(Text, { dimColor: true, children: ["(", device.width, "\u00D7", device.height, ")"] })] })] }), _jsxs(Text, { dimColor: true, children: ["App Platform ID ", result.appPlatformId, " \u00B7 ", host.name] }), _jsxs(Text, { children: ["SDK ", result.configureState, " \u00B7 pinned to @namiml/sdk-core ", result.sdkVersion, " \u00B7 ", result.sdkEnabled ? "SDK enabled" : _jsx(Text, { color: palette.coral, children: "SDK disabled" })] }), result.deviceId && _jsxs(Text, { dimColor: true, children: ["device ", result.deviceId] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["placements ", _jsx(Text, { bold: true, children: counts.placements }), " \u00B7 products ", _jsx(Text, { bold: true, children: counts.products }), " \u00B7 paywalls ", _jsx(Text, { bold: true, children: counts.paywalls })] }) }), shown.length > 0 && (_jsxs(Text, { dimColor: true, children: [shown.join(" · "), more > 0 ? ` · +${more} more` : ""] }))
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: palette.aqua, paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: palette.aqua, children: "Connected" }), " \u00B7 ", app.name, " \u00B7 ", platform.name, " ", _jsxs(Text, { color: palette.secondaryBlue, children: ["[", platform.typeLabel, "]"] }), device && _jsxs(Text, { children: [" \u00B7 ", device.displayName, " ", _jsxs(Text, { dimColor: true, children: ["(", device.width, "\u00D7", device.height, ")"] })] })] }), _jsxs(Text, { dimColor: true, children: ["App Platform ID ", result.appPlatformId, " \u00B7 ", host.name] }), _jsxs(Text, { children: ["SDK ", result.configureState, " \u00B7 pinned to @namiml/sdk-core ", result.sdkVersion, " \u00B7 ", result.sdkEnabled ? "SDK enabled" : _jsx(Text, { color: palette.coral, children: "SDK disabled" })] }), result.deviceId && _jsxs(Text, { dimColor: true, children: ["device ", result.deviceId] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["placements ", _jsx(Text, { bold: true, children: counts.placements }), " \u00B7 products ", _jsx(Text, { bold: true, children: counts.products }), " \u00B7 paywalls ", _jsx(Text, { bold: true, children: counts.paywalls })] }) }), shown.length > 0 && (_jsxs(Text, { dimColor: true, children: [shown.join(" · "), more > 0 ? ` · +${more} more` : ""] }))] }), _jsxs(Text, { dimColor: true, children: ["l launch a placement \u00B7 s switch app \u00B7 p switch platform", canSwitchDevice ? " · d switch device" : "", " \u00B7 q quit"] })] }));
|
|
11
11
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { SelectList } from "../components/SelectList.js";
|
|
4
|
+
const CHOICES = [
|
|
5
|
+
{ key: "signin", label: "Sign in to pick my app", hint: "your Control Center account" },
|
|
6
|
+
{ key: "paste", label: "I have an App Platform ID", hint: "no sign-in needed" },
|
|
7
|
+
{ key: "new", label: "I'm new to Nami", hint: "start here" },
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* First-run welcome: sign in, paste an App Platform ID, or read the explainer.
|
|
11
|
+
* Pure — it never fetches and never exits; `Shell` owns every transition.
|
|
12
|
+
*/
|
|
13
|
+
export function WelcomeScreen({ host, onChoose, onCancel }) {
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [host.name, " \u00B7 ", host.baseUrl] }), _jsx(SelectList, { title: "Welcome to Nami", items: CHOICES, initialKey: "signin", onSelect: (key) => onChoose(key), onCancel: onCancel })] }));
|
|
15
|
+
}
|
package/dist/sdk/summary.js
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The one human label a campaign rule goes by: the placement label for label
|
|
3
|
+
* rules, else the rule name. Empty when the rule carries neither.
|
|
4
|
+
*
|
|
5
|
+
* This is the single copy of that rule — `summarizePlacements()` (the configure
|
|
6
|
+
* summary and the Success screen) and `listPlacements()` (`src/placements/list.ts`,
|
|
7
|
+
* the picker and `nami placements`) both go through it so they cannot drift.
|
|
8
|
+
*/
|
|
9
|
+
export function placementLabel(campaign) {
|
|
10
|
+
const label = campaign.type === "label" && typeof campaign.value === "string" && campaign.value.trim() ? campaign.value.trim() : campaign.name?.trim();
|
|
11
|
+
return label || undefined;
|
|
12
|
+
}
|
|
13
|
+
/** One human label per campaign rule, de-duplicated and sorted. */
|
|
2
14
|
export function summarizePlacements(campaigns) {
|
|
3
15
|
if (!Array.isArray(campaigns))
|
|
4
16
|
return [];
|
|
5
17
|
const labels = new Set();
|
|
6
18
|
for (const c of campaigns) {
|
|
7
|
-
const label =
|
|
19
|
+
const label = placementLabel(c);
|
|
8
20
|
if (label)
|
|
9
21
|
labels.add(label);
|
|
10
22
|
}
|
|
11
23
|
return [...labels].sort((a, b) => a.localeCompare(b));
|
|
12
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The two lines every non-JSON configure path prints. `nami configure`, `nami
|
|
27
|
+
* onboarding` and any future caller share this so their output cannot drift.
|
|
28
|
+
*/
|
|
29
|
+
export function configureSummaryLines(result) {
|
|
30
|
+
return [
|
|
31
|
+
`Configured ${result.appPlatformId} on ${result.host}: ${result.configureState}${result.sdkEnabled ? "" : " (SDK disabled)"}${result.device ? ` · ${result.device.displayName}` : ""}`,
|
|
32
|
+
`placements ${result.counts.placements} · products ${result.counts.products} · paywalls ${result.counts.paywalls}${result.deviceId ? ` · device ${result.deviceId}` : ""}`,
|
|
33
|
+
];
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const nodeSpawn = (cmd, args, opts) => spawn(cmd, args, opts);
|
|
3
|
+
/** The platform's "open this in the default handler" command, or null when we have none. */
|
|
4
|
+
function opener(platform, url) {
|
|
5
|
+
switch (platform) {
|
|
6
|
+
case "darwin":
|
|
7
|
+
return { cmd: "open", args: [url] };
|
|
8
|
+
case "win32":
|
|
9
|
+
// The empty string is `start`'s title argument; without it a quoted URL becomes the title.
|
|
10
|
+
return { cmd: "cmd", args: ["/c", "start", "", url] };
|
|
11
|
+
case "linux":
|
|
12
|
+
return { cmd: "xdg-open", args: [url] };
|
|
13
|
+
default:
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Best effort: open `url` in the developer's browser. Never throws and never blocks —
|
|
19
|
+
* returns false when we have no opener for this platform or spawning fails, so the
|
|
20
|
+
* caller can print the URL instead.
|
|
21
|
+
*/
|
|
22
|
+
export function openUrl(url, platform = process.platform, spawnImpl = nodeSpawn) {
|
|
23
|
+
const target = opener(platform, url);
|
|
24
|
+
if (!target)
|
|
25
|
+
return false;
|
|
26
|
+
try {
|
|
27
|
+
spawnImpl(target.cmd, target.args, { stdio: "ignore", detached: true }).unref();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
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.202609231629";
|
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.202609231629",
|
|
4
4
|
"description": "Nami SDK developer CLI — terminal test app, implementation and flow checks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nami",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@namiml/sdk-core": "3.4.11-dev.
|
|
38
|
+
"@namiml/sdk-core": "3.4.11-dev.202609231629",
|
|
39
39
|
"commander": "^15",
|
|
40
40
|
"env-paths": "^4",
|
|
41
41
|
"ink": "^6.0.0",
|