@namiml/cli 3.4.11-dev.202609221916

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +21 -0
  3. package/dist/App.js +22 -0
  4. package/dist/adapters/file-storage.adapter.js +87 -0
  5. package/dist/adapters/index.js +18 -0
  6. package/dist/adapters/terminal-device.adapter.js +39 -0
  7. package/dist/adapters/terminal-purchase.adapter.js +12 -0
  8. package/dist/adapters/terminal-storage.adapter.js +19 -0
  9. package/dist/adapters/terminal-ui.adapter.js +23 -0
  10. package/dist/auth/control-center.js +130 -0
  11. package/dist/auth/errors.js +10 -0
  12. package/dist/auth/redact.js +11 -0
  13. package/dist/auth/session.js +56 -0
  14. package/dist/auth/token-store.js +36 -0
  15. package/dist/cli.js +28 -0
  16. package/dist/commands/apps.js +26 -0
  17. package/dist/commands/config.js +20 -0
  18. package/dist/commands/configure.js +29 -0
  19. package/dist/commands/context.js +26 -0
  20. package/dist/commands/list-output.js +9 -0
  21. package/dist/commands/login.js +21 -0
  22. package/dist/commands/logout.js +7 -0
  23. package/dist/commands/onboarding.js +13 -0
  24. package/dist/commands/platforms.js +34 -0
  25. package/dist/commands/shell.js +16 -0
  26. package/dist/commands/whoami.js +42 -0
  27. package/dist/components/SelectList.js +40 -0
  28. package/dist/components/Thinking.js +20 -0
  29. package/dist/components/Wave.js +30 -0
  30. package/dist/components/wave-frame.js +21 -0
  31. package/dist/config/selection.js +22 -0
  32. package/dist/config/store.js +132 -0
  33. package/dist/control-center/apps.js +15 -0
  34. package/dist/control-center/errors.js +10 -0
  35. package/dist/control-center/pagination.js +29 -0
  36. package/dist/control-center/platforms.js +28 -0
  37. package/dist/control-center/resolve.js +35 -0
  38. package/dist/hosts.js +13 -0
  39. package/dist/paths.js +5 -0
  40. package/dist/program.js +100 -0
  41. package/dist/screens/ConfigureScreen.js +53 -0
  42. package/dist/screens/LoginFlow.js +99 -0
  43. package/dist/screens/PickApp.js +11 -0
  44. package/dist/screens/PickPlatform.js +38 -0
  45. package/dist/screens/SelectionHeader.js +8 -0
  46. package/dist/screens/Shell.js +114 -0
  47. package/dist/sdk/configure.js +59 -0
  48. package/dist/sdk/verify.js +42 -0
  49. package/dist/theme.js +22 -0
  50. package/dist/utils/terminal.js +17 -0
  51. package/dist/version.js +2 -0
  52. package/package.json +45 -0
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { Thinking } from "../components/Thinking.js";
5
+ import { palette } from "../theme.js";
6
+ import { configureSdk } from "../sdk/configure.js";
7
+ import { SDK_DISABLED_MESSAGE, verifyAppPlatform } from "../sdk/verify.js";
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 }) {
10
+ const [state, setState] = useState({ kind: "verifying" });
11
+ const run = async (verified) => {
12
+ setState({ kind: "configuring" });
13
+ try {
14
+ onDone(await configure({ host, appPlatformId }, { verified }));
15
+ }
16
+ catch (error) {
17
+ setState({ kind: "failed", message: error instanceof Error ? error.message : String(error) });
18
+ }
19
+ };
20
+ useEffect(() => {
21
+ void (async () => {
22
+ try {
23
+ const verified = await verify(host.baseUrl, appPlatformId);
24
+ if (verified.kind === "sdk_disabled")
25
+ setState({ kind: "disabled", verified });
26
+ else
27
+ await run(verified);
28
+ }
29
+ catch (error) {
30
+ setState({ kind: "failed", message: error instanceof Error ? error.message : String(error) });
31
+ }
32
+ })();
33
+ }, [host.baseUrl, appPlatformId]);
34
+ useInput((_, key) => {
35
+ if (state.kind === "disabled") {
36
+ if (key.return)
37
+ void run(state.verified);
38
+ else if (key.escape)
39
+ onBack?.();
40
+ }
41
+ else if (state.kind === "failed" && (key.escape || key.return)) {
42
+ onBack?.();
43
+ }
44
+ }, { isActive: state.kind === "disabled" || state.kind === "failed" });
45
+ if (state.kind === "verifying")
46
+ return _jsx(Thinking, { label: `Verifying ${appPlatformId} on ${host.name}…`, phase: phase });
47
+ if (state.kind === "configuring")
48
+ return _jsx(Thinking, { label: "Configuring the SDK\u2026", phase: phase });
49
+ if (state.kind === "disabled") {
50
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: palette.coral, children: SDK_DISABLED_MESSAGE }), _jsx(Text, { dimColor: true, children: "Enter to configure anyway \u00B7 Esc to pick another platform" })] }));
51
+ }
52
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: palette.coral, children: state.message }), _jsx(Text, { dimColor: true, children: "Esc to go back" })] }));
53
+ }
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text, useApp } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ import SelectInput from "ink-select-input";
6
+ import { Thinking } from "../components/Thinking.js";
7
+ import { palette } from "../theme.js";
8
+ import { AuthError } from "../auth/errors.js";
9
+ /**
10
+ * Interactive sign-in: email → password → (TOTP) → (org) → tokens saved.
11
+ * Credentials live in component state only until the request that uses them
12
+ * returns; nothing but tokens + org context is ever persisted.
13
+ */
14
+ export function LoginFlow({ auth, store, host, totpCode, org, onDone, onFail, exitWhenDone = true, phase }) {
15
+ const { exit } = useApp();
16
+ const [step, setStep] = useState({ kind: "email" });
17
+ const [draft, setDraft] = useState("");
18
+ const fail = (error) => {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ setStep({ kind: "failed", message });
21
+ onFail?.(error instanceof Error ? error : new Error(message));
22
+ };
23
+ const finish = async (signedIn, chosen) => {
24
+ setStep({ kind: "busy", label: chosen ? `Switching to ${chosen.name}…` : "Finishing sign-in…" });
25
+ try {
26
+ const accessToken = chosen ? await auth.selectOrg(signedIn.tokens.refreshToken, chosen.id) : signedIn.tokens.accessToken;
27
+ const session = {
28
+ host: host.name,
29
+ baseUrl: host.baseUrl,
30
+ accessToken,
31
+ refreshToken: signedIn.tokens.refreshToken,
32
+ orgId: chosen?.id ?? null,
33
+ orgName: chosen?.name ?? null,
34
+ savedAt: new Date().toISOString(),
35
+ };
36
+ await store.save(session);
37
+ setStep({ kind: "done", session });
38
+ onDone?.(session);
39
+ }
40
+ catch (error) {
41
+ fail(error);
42
+ }
43
+ };
44
+ const chooseOrg = (signedIn) => {
45
+ const orgs = signedIn.profile.orgs;
46
+ if (org) {
47
+ const match = orgs.find((o) => o.id === org || o.name === org);
48
+ if (!match)
49
+ return fail(new AuthError("org_forbidden", `Your account is not a member of "${org}".`));
50
+ return void finish(signedIn, match);
51
+ }
52
+ if (orgs.length === 0)
53
+ return fail(new AuthError("org_forbidden", "Your account does not belong to any organization yet."));
54
+ if (orgs.length === 1)
55
+ return void finish(signedIn, orgs[0]);
56
+ setStep({ kind: "org", signedIn });
57
+ };
58
+ const submitPassword = async (email, password) => {
59
+ setDraft("");
60
+ setStep({ kind: "busy", label: "Signing in…" });
61
+ try {
62
+ const result = await auth.login(email, password);
63
+ if (result.kind === "signed_in")
64
+ return chooseOrg(result);
65
+ if (totpCode)
66
+ return chooseOrg(await auth.confirmMfa(result.preAuthToken, totpCode));
67
+ setStep({ kind: "mfa", preAuthToken: result.preAuthToken });
68
+ }
69
+ catch (error) {
70
+ if (error instanceof AuthError && error.code === "invalid_credentials") {
71
+ return setStep({ kind: "password", email, notice: error.message });
72
+ }
73
+ fail(error);
74
+ }
75
+ };
76
+ const submitCode = async (preAuthToken, code) => {
77
+ setDraft("");
78
+ setStep({ kind: "busy", label: "Checking your code…" });
79
+ try {
80
+ chooseOrg(await auth.confirmMfa(preAuthToken, code));
81
+ }
82
+ catch (error) {
83
+ if (error instanceof AuthError && error.code === "mfa_invalid") {
84
+ return setStep({ kind: "mfa", preAuthToken, notice: error.message });
85
+ }
86
+ fail(error);
87
+ }
88
+ };
89
+ useEffect(() => {
90
+ if (exitWhenDone && (step.kind === "done" || step.kind === "failed"))
91
+ exit();
92
+ }, [step.kind, exitWhenDone, exit]);
93
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Sign in to Nami" }), _jsx(Text, { dimColor: true, children: host.baseUrl }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [step.kind === "email" && (_jsxs(Box, { children: [_jsx(Text, { children: "Email: " }), _jsx(TextInput, { value: draft, onChange: setDraft, onSubmit: (v) => { const email = v.trim(); if (email) {
94
+ setDraft("");
95
+ setStep({ kind: "password", email });
96
+ } } })] })), step.kind === "password" && (_jsxs(Box, { flexDirection: "column", children: [step.notice && _jsx(Text, { color: palette.aqua, children: step.notice }), _jsxs(Box, { children: [_jsx(Text, { children: "Password: " }), _jsx(TextInput, { value: draft, onChange: setDraft, mask: "*", onSubmit: (v) => { if (v)
97
+ void submitPassword(step.email, v); } })] })] })), step.kind === "mfa" && (_jsxs(Box, { flexDirection: "column", children: [step.notice && _jsx(Text, { color: palette.aqua, children: step.notice }), _jsxs(Box, { children: [_jsx(Text, { children: "Authenticator code: " }), _jsx(TextInput, { value: draft, onChange: setDraft, onSubmit: (v) => { const code = v.trim(); if (code)
98
+ void submitCode(step.preAuthToken, code); } })] })] })), step.kind === "org" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Choose an organization:" }), _jsx(SelectInput, { items: step.signedIn.profile.orgs.map((o) => ({ key: o.id, label: o.name, value: o.id })), initialIndex: Math.max(0, step.signedIn.profile.orgs.findIndex((o) => o.id === step.signedIn.profile.defaultOrgId)), onSelect: (item) => { const chosen = step.signedIn.profile.orgs.find((o) => o.id === item.value); void finish(step.signedIn, chosen); } })] })), step.kind === "busy" && _jsx(Thinking, { label: step.label, phase: phase }), step.kind === "done" && (_jsxs(Text, { color: palette.aqua, children: ["Signed in", step.session.orgName ? ` to ${step.session.orgName}` : "", " on ", step.session.host, "."] })), step.kind === "failed" && _jsxs(Text, { color: palette.coral, children: ["Sign-in failed: ", step.message] })] })] }));
99
+ }
@@ -0,0 +1,11 @@
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
+ export function PickApp({ apps, orgName, baseUrl, currentAppId, onSelect, onCancel }) {
5
+ const where = orgName ? `in ${orgName}` : "in your organization";
6
+ if (apps.length === 0) {
7
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["No apps ", where, " yet."] }), _jsxs(Text, { children: ["Create one in the Control Center", baseUrl ? `: ${baseUrl}` : "", ", then run nami again."] })] }));
8
+ }
9
+ return (_jsx(SelectList, { title: `Apps ${where}`, items: apps.map((a) => ({ key: a.id, label: a.name, hint: a.defaultLanguage ?? undefined })), initialKey: currentAppId, onSelect: (key) => { const app = apps.find((a) => a.id === key); if (app)
10
+ onSelect(app); }, onCancel: onCancel }));
11
+ }
@@ -0,0 +1,38 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { SelectList } from "../components/SelectList.js";
5
+ import { palette } from "../theme.js";
6
+ const flags = (p) => [p.sdkEnabled ? "SDK enabled" : "SDK disabled", p.isConfigValid ? "config valid" : "config invalid"].join(" · ");
7
+ export function PickPlatform({ appName, platforms, currentPlatformId, onSelect, onCancel }) {
8
+ const [pending, setPending] = useState(null);
9
+ useInput((_, key) => {
10
+ if (!pending)
11
+ return;
12
+ if (key.return)
13
+ onSelect(pending);
14
+ else if (key.escape)
15
+ setPending(null);
16
+ }, { isActive: pending !== null });
17
+ if (pending) {
18
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: palette.coral, children: [pending.name, " has the SDK disabled \u2014 configure() will fail until the SDK is enabled in the Control Center."] }), _jsx(Text, { dimColor: true, children: "Enter to continue anyway \u00B7 Esc to pick another platform" })] }));
19
+ }
20
+ if (platforms.length === 0) {
21
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [appName, " has no platforms yet."] }), _jsx(Text, { children: "Add one in the Control Center, then run nami again." })] }));
22
+ }
23
+ return (_jsx(SelectList, { title: `Platforms for ${appName}`, items: platforms.map((p) => ({
24
+ key: p.id,
25
+ label: p.name,
26
+ badge: p.typeLabel,
27
+ hint: p.sdkEnabled ? flags(p) : undefined,
28
+ warn: p.sdkEnabled ? undefined : flags(p),
29
+ })), initialKey: currentPlatformId, onSelect: (key) => {
30
+ const platform = platforms.find((p) => p.id === key);
31
+ if (!platform)
32
+ return;
33
+ if (platform.sdkEnabled)
34
+ onSelect(platform);
35
+ else
36
+ setPending(platform);
37
+ }, onCancel: onCancel }));
38
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { palette } from "../theme.js";
4
+ /** One-line context bar: `org / app / platform (type)`. Always visible in the shell. */
5
+ export function SelectionHeader({ orgName, appName, platformName, platformType, host }) {
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, "]"] })] }));
8
+ }
@@ -0,0 +1,114 @@
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";
4
+ import { LoginFlow } from "./LoginFlow.js";
5
+ import { PickApp } from "./PickApp.js";
6
+ import { PickPlatform } from "./PickPlatform.js";
7
+ import { SelectionHeader } from "./SelectionHeader.js";
8
+ import { Thinking } from "../components/Thinking.js";
9
+ import { palette } from "../theme.js";
10
+ import { AuthError } from "../auth/errors.js";
11
+ import { listApps } from "../control-center/apps.js";
12
+ import { listPlatforms } from "../control-center/platforms.js";
13
+ import { findApp, findPlatform, ResolveError } from "../control-center/resolve.js";
14
+ import { ConfigureScreen } from "./ConfigureScreen.js";
15
+ import { appFrom, platformFrom, selectionFrom } from "../config/selection.js";
16
+ /**
17
+ * Bare `nami`: sign in if needed → pick app → pick platform → home.
18
+ * Selection is in-memory for now (persistence: NAM-4501; configure: NAM-4500;
19
+ * full Switch with Recent/org switching: NAM-4503).
20
+ */
21
+ export function Shell({ ctx, appQuery, platformQuery, onExit, exitWhenDone = true, phase, verify, configure }) {
22
+ const { exit } = useApp();
23
+ const [state, setState] = useState({ kind: "boot" });
24
+ const [orgName, setOrgName] = useState(null);
25
+ const [orgId, setOrgId] = useState(null);
26
+ const step = (name) => void ctx.config.setOnboardingStep(name).catch(() => { });
27
+ const [queries, setQueries] = useState({ app: appQuery, platform: platformQuery });
28
+ const finish = (code) => {
29
+ onExit?.(code);
30
+ if (exitWhenDone)
31
+ exit();
32
+ };
33
+ const fatal = (message, code = 1) => setState({ kind: "fatal", message, code });
34
+ const handleError = (error) => {
35
+ if (error instanceof AuthError && error.code === "session_expired")
36
+ return setState({ kind: "signin", notice: error.message });
37
+ if (error instanceof ResolveError)
38
+ return fatal(error.message, 2);
39
+ fatal(error instanceof Error ? error.message : String(error));
40
+ };
41
+ const loadApps = async (current) => {
42
+ setState({ kind: "busy", label: "Loading your apps…" });
43
+ try {
44
+ const apps = await listApps(ctx.session);
45
+ if (queries.app) {
46
+ const app = findApp(apps, queries.app);
47
+ setQueries((q) => ({ ...q, app: undefined }));
48
+ return void loadPlatforms(app);
49
+ }
50
+ step("pick-app");
51
+ setState({ kind: "pick-app", apps, current });
52
+ }
53
+ catch (error) {
54
+ handleError(error);
55
+ }
56
+ };
57
+ const loadPlatforms = async (app, current) => {
58
+ setState({ kind: "busy", label: `Loading platforms for ${app.name}…` });
59
+ try {
60
+ const platforms = await listPlatforms(ctx.session, app.id);
61
+ if (queries.platform) {
62
+ const platform = findPlatform(platforms, queries.platform);
63
+ setQueries((q) => ({ ...q, platform: undefined }));
64
+ return setState({ kind: "configure", app, platform });
65
+ }
66
+ step("pick-platform");
67
+ setState({ kind: "pick-platform", app, platforms, current });
68
+ }
69
+ catch (error) {
70
+ handleError(error);
71
+ }
72
+ };
73
+ useEffect(() => {
74
+ void (async () => {
75
+ const stored = await ctx.store.load();
76
+ setOrgName(stored?.orgName ?? null);
77
+ setOrgId(stored?.orgId ?? null);
78
+ const signedIn = Boolean(stored || ctx.env.NAMI_TOKEN);
79
+ if (!signedIn) {
80
+ step("signin");
81
+ return setState({ kind: "signin" });
82
+ }
83
+ const { config } = await ctx.config.load();
84
+ // Zero-prompt re-run: a persisted selection (and no --app/--platform override) goes straight to configure.
85
+ if (config.current && !queries.app && !queries.platform) {
86
+ step("configure");
87
+ return setState({ kind: "configure", app: appFrom(config.current), platform: platformFrom(config.current) });
88
+ }
89
+ await loadApps();
90
+ })();
91
+ }, []);
92
+ useEffect(() => {
93
+ if (state.kind === "fatal")
94
+ finish(state.code);
95
+ }, [state.kind]);
96
+ useInput((input, key) => {
97
+ if (state.kind !== "home")
98
+ return;
99
+ if (input === "q" || key.escape)
100
+ return finish(0);
101
+ if (input === "s")
102
+ return void loadApps(state.app);
103
+ if (input === "p")
104
+ return void loadPlatforms(state.app, state.platform);
105
+ }, { 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 loadApps(state.app) })), state.kind === "configure" && (_jsx(ConfigureScreen, { host: ctx.host, appPlatformId: state.platform.id, phase: phase, verify: verify, configure: configure, onDone: (result) => {
108
+ void ctx.config
109
+ .setCurrent(selectionFrom(state.app, state.platform), { host: ctx.host.name, session: { orgId, orgName } })
110
+ .then(() => ctx.config.markOnboardingDone())
111
+ .catch(() => { });
112
+ setState({ kind: "home", app: state.app, platform: state.platform, result });
113
+ }, onBack: () => void loadPlatforms(state.app, state.platform) })), 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 })] }), _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." })] }), _jsx(Text, { dimColor: true, children: "s switch app \u00B7 p switch platform \u00B7 q quit" })] })), state.kind === "fatal" && _jsx(Text, { color: palette.coral, children: state.message })] })] }));
114
+ }
@@ -0,0 +1,59 @@
1
+ import { Nami, NamiCampaignManager, NamiPaywallManager, storageService } from "@namiml/sdk-core";
2
+ import { registerTerminalAdapters } from "../adapters/index.js";
3
+ import { FileStorageAdapter, storagePathFor } from "../adapters/file-storage.adapter.js";
4
+ import { configDir } from "../paths.js";
5
+ import { verifyAppPlatform } from "./verify.js";
6
+ let activeStorage = null;
7
+ let configuredOnce = false;
8
+ /** The storage adapter registered by the last configureSdk() call (tests/inspection). */
9
+ export const currentStorage = () => activeStorage;
10
+ /**
11
+ * Verify the App Platform ID, register the terminal adapters with a per-platform
12
+ * file store, then run core's configure(). Safe to call again for another
13
+ * platform: the previous session is reset first and its store flushed.
14
+ */
15
+ export async function configureSdk(input, deps = {}) {
16
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
17
+ const verified = deps.verified ?? (await verifyAppPlatform(input.host.baseUrl, input.appPlatformId, fetchImpl));
18
+ const storagePath = storagePathFor(input.appPlatformId, deps.configDir ?? configDir());
19
+ if (configuredOnce)
20
+ await Nami.reset();
21
+ activeStorage?.dispose();
22
+ activeStorage = new FileStorageAdapter(storagePath);
23
+ registerTerminalAdapters({ storage: activeStorage });
24
+ const namiCommands = input.host.name === "staging" ? ["useStagingAPI"] : [];
25
+ const state = await Nami.configure({ appPlatformID: input.appPlatformId, namiCommands, logLevel: input.logLevel ?? "warn" });
26
+ configuredOnce = true;
27
+ activeStorage.flush();
28
+ return {
29
+ appPlatformId: input.appPlatformId,
30
+ host: input.host.name,
31
+ sdkEnabled: verified.kind === "ok",
32
+ configureState: state.configureState,
33
+ sdkInitialized: state.sdkInitialized,
34
+ counts: {
35
+ placements: safeCount(() => NamiCampaignManager.allCampaigns().length),
36
+ products: safeCount(() => NamiCampaignManager.getAllProducts().length),
37
+ paywalls: safeCount(() => NamiPaywallManager.allPaywalls().length),
38
+ },
39
+ deviceId: safeDeviceId(),
40
+ storagePath,
41
+ };
42
+ }
43
+ function safeCount(read) {
44
+ try {
45
+ return read();
46
+ }
47
+ catch {
48
+ return 0;
49
+ }
50
+ }
51
+ function safeDeviceId() {
52
+ try {
53
+ const device = storageService.getDevice();
54
+ return typeof device?.id === "string" ? device.id : null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
@@ -0,0 +1,42 @@
1
+ export class VerifyError extends Error {
2
+ code;
3
+ status;
4
+ constructor(code, message, status) {
5
+ super(message);
6
+ this.code = code;
7
+ this.status = status;
8
+ this.name = "VerifyError";
9
+ }
10
+ }
11
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
+ export const isAppPlatformId = (value) => UUID.test(value.trim());
13
+ const hostLabel = (baseUrl) => (baseUrl.includes("staging") ? "staging" : "production");
14
+ /**
15
+ * `GET /sdk/v3/platform/<id>/config/` — public SDK endpoint, no auth. 200 proves the
16
+ * App Platform ID exists on this host; "SDK disabled" is the absence of the `sdk`
17
+ * capability (the same rule Apple's `updateSdkFlags` applies).
18
+ */
19
+ export async function verifyAppPlatform(baseUrl, appPlatformId, fetchImpl = globalThis.fetch) {
20
+ const id = appPlatformId.trim();
21
+ if (!isAppPlatformId(id)) {
22
+ throw new VerifyError("invalid_id", `"${appPlatformId}" is not an App Platform ID (expected a UUID — copy it from the Control Center or nami platforms).`);
23
+ }
24
+ const host = hostLabel(baseUrl);
25
+ let res;
26
+ try {
27
+ res = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/sdk/v3/platform/${id}/config/`, { headers: { accept: "application/json" } });
28
+ }
29
+ catch {
30
+ throw new VerifyError("network", `Could not reach ${baseUrl}. Check your connection and the host.`);
31
+ }
32
+ if (res.status === 404) {
33
+ const other = host === "staging" ? "production (drop --host staging)" : "staging (--host staging)";
34
+ throw new VerifyError("not_found", `No app platform with ID ${id} on ${host}. Check the ID in the Control Center or try ${other}.`, 404);
35
+ }
36
+ if (!res.ok)
37
+ throw new VerifyError("http", `Config lookup on ${host} failed (HTTP ${res.status}). Try again in a moment.`, res.status);
38
+ const config = (await res.json());
39
+ const capabilities = Array.isArray(config.capabilities) ? config.capabilities : [];
40
+ return { kind: capabilities.includes("sdk") ? "ok" : "sdk_disabled", config: { ...config, capabilities } };
41
+ }
42
+ export const SDK_DISABLED_MESSAGE = "The SDK is disabled for this platform in the Control Center (Platforms → SDK enabled). configure() will not load campaigns until it is enabled.";
package/dist/theme.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Nami brand palette. Hex values are verbatim from the nami-foundations
3
+ * `brand-identity` skill (canonical source: reef `context/brand/nami-brand.md`).
4
+ * Coral / Accent Red are alert colours: errors and failures only, never decoration.
5
+ */
6
+ export const palette = {
7
+ navy: "#021f3e",
8
+ blueAlloy: "#0b4a8e",
9
+ primaryBlue: "#1374de",
10
+ secondaryBlue: "#009fde",
11
+ aqua: "#67d6e0",
12
+ mediumGray: "#8c8c8c",
13
+ /** Alerts and errors only. */
14
+ coral: "#ff5b74",
15
+ };
16
+ /** Wave colour bands, trough → crest. */
17
+ export const WAVE_COLORS = [
18
+ palette.blueAlloy,
19
+ palette.primaryBlue,
20
+ palette.secondaryBlue,
21
+ palette.aqua,
22
+ ];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Whether it is appropriate to animate: an interactive TTY, not CI, and the
3
+ * user has not asked for a quiet terminal via NO_COLOR (any non-empty value).
4
+ * Everything animated in the CLI gates on this and renders one static frame otherwise.
5
+ */
6
+ export function shouldAnimate(env = process.env, stdout = process.stdout) {
7
+ if (!stdout.isTTY)
8
+ return false;
9
+ if (env.CI)
10
+ return false;
11
+ if (env.NO_COLOR)
12
+ return false;
13
+ return true;
14
+ }
15
+ export function terminalColumns(stdout = process.stdout, fallback = 80) {
16
+ return stdout.columns && stdout.columns > 0 ? stdout.columns : fallback;
17
+ }
@@ -0,0 +1,2 @@
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.202609221916";
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@namiml/cli",
3
+ "version": "3.4.11-dev.202609221916",
4
+ "description": "Nami SDK developer CLI — terminal test app, implementation and flow checks",
5
+ "keywords": [
6
+ "nami",
7
+ "paywall",
8
+ "subscriptions",
9
+ "cli",
10
+ "test-app",
11
+ "flows"
12
+ ],
13
+ "type": "module",
14
+ "bin": {
15
+ "nami": "./dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "license": "SEE LICENSE FILE",
24
+ "author": {
25
+ "name": "Nami ML Inc.",
26
+ "email": "hello@namiml.com"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/namiml/nami-cli.git"
31
+ },
32
+ "homepage": "https://www.namiml.com",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@namiml/sdk-core": "3.4.11-dev.202609221916",
38
+ "commander": "^15",
39
+ "env-paths": "^4",
40
+ "ink": "^6.0.0",
41
+ "ink-select-input": "^6",
42
+ "ink-text-input": "^6",
43
+ "react": "^19.0.0"
44
+ }
45
+ }