@salestouch/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/dist/core.d.ts +91 -0
- package/dist/core.js +382 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +937 -0
- package/dist/locale.d.ts +5 -0
- package/dist/locale.js +28 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +181 -0
- package/dist/setup-wizard.d.ts +39 -0
- package/dist/setup-wizard.js +492 -0
- package/dist/setup.d.ts +51 -0
- package/dist/setup.js +473 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# SalesTouch CLI
|
|
2
|
+
|
|
3
|
+
Public CLI for SalesTouch commands, resources, agent setup, and MCP workflows.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm salestouch --help
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The npm package is not published yet. Run the CLI from the repository root with `pnpm salestouch ...`.
|
|
12
|
+
|
|
13
|
+
## Authenticate
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm salestouch auth login
|
|
17
|
+
pnpm salestouch auth whoami
|
|
18
|
+
pnpm salestouch auth logout
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
API key authentication is also supported:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm salestouch auth use-key st_...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm salestouch doctor
|
|
31
|
+
pnpm salestouch commands list
|
|
32
|
+
pnpm salestouch commands schema mission.get
|
|
33
|
+
pnpm salestouch commands run mission.get --input-json '{"mission_id":"mission_123"}'
|
|
34
|
+
pnpm salestouch resources list
|
|
35
|
+
pnpm salestouch resources read salestouch://status
|
|
36
|
+
pnpm salestouch setup --codex
|
|
37
|
+
pnpm salestouch mcp serve
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The CLI can authenticate with either a stored SalesTouch login session or an API key. `salestouch setup` persists the selected auth method to the CLI config so local MCP clients can reuse it.
|
|
41
|
+
By default, `salestouch setup` installs `CLI + skills`. Add `--mcp` when you want MCP instead.
|
|
42
|
+
If you run `pnpm salestouch setup` with no setup flags in an interactive terminal, the CLI starts a small guided setup wizard.
|
|
43
|
+
|
|
44
|
+
## Setup
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Project-local CLI + skills setup for Codex
|
|
48
|
+
pnpm salestouch setup --codex
|
|
49
|
+
|
|
50
|
+
# Configure Claude and Cursor explicitly in CLI mode
|
|
51
|
+
pnpm salestouch setup --claude --cursor --cli
|
|
52
|
+
|
|
53
|
+
# Opt into remote MCP explicitly
|
|
54
|
+
pnpm salestouch setup --claude --mcp --login
|
|
55
|
+
|
|
56
|
+
# Force login during setup
|
|
57
|
+
pnpm salestouch setup --codex --login
|
|
58
|
+
|
|
59
|
+
# Persist an API key during setup
|
|
60
|
+
pnpm salestouch setup --codex --api-key st_...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## MCP
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Start the local stdio MCP server
|
|
67
|
+
pnpm salestouch mcp serve
|
|
68
|
+
|
|
69
|
+
# Override auth explicitly for the MCP process
|
|
70
|
+
pnpm salestouch mcp serve --api-key st_...
|
|
71
|
+
pnpm salestouch mcp serve --access-token <token>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Environment
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export SALESTOUCH_API_BASE_URL="http://localhost:3000"
|
|
78
|
+
export SALESTOUCH_API_KEY="st_..."
|
|
79
|
+
export SALESTOUCH_ACCESS_TOKEN="<token>"
|
|
80
|
+
```
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type FlagValue = string | boolean;
|
|
2
|
+
export type CliConfig = {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
accessToken?: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ResolvedBaseUrl = {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
baseUrlSource: "flag" | "env" | "config" | "workspace" | "default";
|
|
10
|
+
};
|
|
11
|
+
export type ResolvedApiKeyAuth = ResolvedBaseUrl & {
|
|
12
|
+
authMethod: "api_key";
|
|
13
|
+
credentialSource: "flag" | "env" | "config";
|
|
14
|
+
apiKey: string;
|
|
15
|
+
};
|
|
16
|
+
export type ResolvedSessionAuth = ResolvedBaseUrl & {
|
|
17
|
+
authMethod: "session";
|
|
18
|
+
credentialSource: "flag" | "env" | "config";
|
|
19
|
+
accessToken: string;
|
|
20
|
+
};
|
|
21
|
+
export type ResolvedAuth = ResolvedApiKeyAuth | ResolvedSessionAuth;
|
|
22
|
+
export type JsonRecord = Record<string, unknown>;
|
|
23
|
+
export declare const CLI_VERSION = "0.3.0";
|
|
24
|
+
export declare const DEFAULT_BASE_URL = "https://www.salestouch.io";
|
|
25
|
+
export declare const CLI_DEVICE_CLIENT_ID = "salestouch-cli";
|
|
26
|
+
export declare function readFlagString(flags: Record<string, FlagValue>, name: string): string | null;
|
|
27
|
+
export declare function readFlagBoolean(flags: Record<string, FlagValue>, name: string): boolean;
|
|
28
|
+
export declare function readEnvString(name: string): string | null;
|
|
29
|
+
export declare function getConfigDirectory(): string;
|
|
30
|
+
export declare function getConfigPath(): string;
|
|
31
|
+
export declare function loadConfig(): Promise<CliConfig>;
|
|
32
|
+
export declare function saveConfig(config: CliConfig): Promise<void>;
|
|
33
|
+
export declare function resolveBaseUrl(overrideBaseUrl?: string | null): Promise<ResolvedBaseUrl>;
|
|
34
|
+
export declare function getRemoteMcpUrl(baseUrl: string): string;
|
|
35
|
+
export declare function resolveAuth(options?: {
|
|
36
|
+
apiKey?: string | null;
|
|
37
|
+
accessToken?: string | null;
|
|
38
|
+
baseUrl?: string | null;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
baseUrlSource: "flag" | "env" | "config" | "workspace" | "default";
|
|
42
|
+
authMethod: "api_key";
|
|
43
|
+
credentialSource: "flag";
|
|
44
|
+
apiKey: string;
|
|
45
|
+
} | {
|
|
46
|
+
baseUrl: string;
|
|
47
|
+
baseUrlSource: "flag" | "env" | "config" | "workspace" | "default";
|
|
48
|
+
authMethod: "api_key";
|
|
49
|
+
credentialSource: "env";
|
|
50
|
+
apiKey: string;
|
|
51
|
+
} | {
|
|
52
|
+
baseUrl: string;
|
|
53
|
+
baseUrlSource: "flag" | "env" | "config" | "workspace" | "default";
|
|
54
|
+
authMethod: "session";
|
|
55
|
+
credentialSource: "flag";
|
|
56
|
+
accessToken: string;
|
|
57
|
+
} | {
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
baseUrlSource: "flag" | "env" | "config" | "workspace" | "default";
|
|
60
|
+
authMethod: "session";
|
|
61
|
+
credentialSource: "env";
|
|
62
|
+
accessToken: string;
|
|
63
|
+
} | {
|
|
64
|
+
baseUrl: string;
|
|
65
|
+
baseUrlSource: "flag" | "env" | "config" | "workspace" | "default";
|
|
66
|
+
authMethod: "session";
|
|
67
|
+
credentialSource: "config";
|
|
68
|
+
accessToken: string;
|
|
69
|
+
} | {
|
|
70
|
+
baseUrl: string;
|
|
71
|
+
baseUrlSource: "flag" | "env" | "config" | "workspace" | "default";
|
|
72
|
+
authMethod: "api_key";
|
|
73
|
+
credentialSource: "config";
|
|
74
|
+
apiKey: string;
|
|
75
|
+
}>;
|
|
76
|
+
export declare function persistAuth(auth: ResolvedAuth): Promise<void>;
|
|
77
|
+
export declare function clearStoredSession(): Promise<void>;
|
|
78
|
+
export declare function clearStoredApiKey(): Promise<void>;
|
|
79
|
+
export declare function clearStoredAuth(): Promise<void>;
|
|
80
|
+
export declare function apiRequest<T>(auth: ResolvedAuth, pathName: string, init?: RequestInit): Promise<T>;
|
|
81
|
+
export declare function outputJson(value: unknown): void;
|
|
82
|
+
export declare function printTable(rows: string[][]): void;
|
|
83
|
+
export declare function printResourceStatus(resource: JsonRecord): void;
|
|
84
|
+
export declare function describeAuthMethod(auth: ResolvedAuth): "api_key" | "salestouch_login";
|
|
85
|
+
export declare function loginWithDeviceFlow(options?: {
|
|
86
|
+
baseUrl?: string | null;
|
|
87
|
+
quiet?: boolean;
|
|
88
|
+
}): Promise<{
|
|
89
|
+
auth: ResolvedSessionAuth;
|
|
90
|
+
userCode: string;
|
|
91
|
+
}>;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
+
import { parse as parseDotenv } from "dotenv";
|
|
8
|
+
export const CLI_VERSION = "0.3.0";
|
|
9
|
+
export const DEFAULT_BASE_URL = "https://www.salestouch.io";
|
|
10
|
+
export const CLI_DEVICE_CLIENT_ID = "salestouch-cli";
|
|
11
|
+
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
12
|
+
const WORKSPACE_BASE_URL_FILES = [".env.local", ".env"];
|
|
13
|
+
export function readFlagString(flags, name) {
|
|
14
|
+
const value = flags[name];
|
|
15
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
16
|
+
}
|
|
17
|
+
export function readFlagBoolean(flags, name) {
|
|
18
|
+
return flags[name] === true;
|
|
19
|
+
}
|
|
20
|
+
export function readEnvString(name) {
|
|
21
|
+
const value = process.env[name]?.trim();
|
|
22
|
+
return value && value.length > 0 ? value : null;
|
|
23
|
+
}
|
|
24
|
+
export function getConfigDirectory() {
|
|
25
|
+
const override = readEnvString("SALESTOUCH_CONFIG_DIR");
|
|
26
|
+
if (override) {
|
|
27
|
+
return override;
|
|
28
|
+
}
|
|
29
|
+
if (process.platform === "darwin") {
|
|
30
|
+
return path.join(os.homedir(), "Library", "Application Support", "SalesTouch");
|
|
31
|
+
}
|
|
32
|
+
if (process.platform === "win32") {
|
|
33
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "SalesTouch");
|
|
34
|
+
}
|
|
35
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "salestouch");
|
|
36
|
+
}
|
|
37
|
+
export function getConfigPath() {
|
|
38
|
+
return path.join(getConfigDirectory(), "cli.json");
|
|
39
|
+
}
|
|
40
|
+
export async function loadConfig() {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readFile(getConfigPath(), "utf8");
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
return {
|
|
45
|
+
apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : undefined,
|
|
46
|
+
accessToken: typeof parsed.accessToken === "string" ? parsed.accessToken : undefined,
|
|
47
|
+
baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function saveConfig(config) {
|
|
55
|
+
const configPath = getConfigPath();
|
|
56
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
57
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
58
|
+
}
|
|
59
|
+
function resolveBaseUrlFromSources(flagBaseUrl, envBaseUrl, configBaseUrl, workspaceBaseUrl) {
|
|
60
|
+
return {
|
|
61
|
+
baseUrl: (flagBaseUrl || envBaseUrl || configBaseUrl || workspaceBaseUrl || DEFAULT_BASE_URL).replace(/\/$/, ""),
|
|
62
|
+
baseUrlSource: flagBaseUrl
|
|
63
|
+
? "flag"
|
|
64
|
+
: envBaseUrl
|
|
65
|
+
? "env"
|
|
66
|
+
: configBaseUrl
|
|
67
|
+
? "config"
|
|
68
|
+
: workspaceBaseUrl
|
|
69
|
+
? "workspace"
|
|
70
|
+
: "default",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function readWorkspaceBaseUrlFromFile(filePath) {
|
|
74
|
+
try {
|
|
75
|
+
const raw = await readFile(filePath, "utf8");
|
|
76
|
+
const parsed = parseDotenv(raw);
|
|
77
|
+
for (const key of ["SALESTOUCH_API_BASE_URL", "NEXT_PUBLIC_APP_URL", "BETTER_AUTH_URL"]) {
|
|
78
|
+
const value = parsed[key]?.trim();
|
|
79
|
+
if (value) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function findWorkspaceBaseUrl(startDir) {
|
|
90
|
+
let currentDir = path.resolve(startDir);
|
|
91
|
+
while (true) {
|
|
92
|
+
for (const fileName of WORKSPACE_BASE_URL_FILES) {
|
|
93
|
+
const filePath = path.join(currentDir, fileName);
|
|
94
|
+
const baseUrl = await readWorkspaceBaseUrlFromFile(filePath);
|
|
95
|
+
if (baseUrl) {
|
|
96
|
+
return baseUrl;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const parentDir = path.dirname(currentDir);
|
|
100
|
+
if (parentDir === currentDir) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
currentDir = parentDir;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function resolveBaseUrl(overrideBaseUrl) {
|
|
107
|
+
const config = await loadConfig();
|
|
108
|
+
const workspaceBaseUrl = await findWorkspaceBaseUrl(process.cwd());
|
|
109
|
+
return resolveBaseUrlFromSources(overrideBaseUrl ?? null, readEnvString("SALESTOUCH_API_BASE_URL"), config.baseUrl?.trim() || null, workspaceBaseUrl);
|
|
110
|
+
}
|
|
111
|
+
export function getRemoteMcpUrl(baseUrl) {
|
|
112
|
+
return `${baseUrl.replace(/\/$/, "")}/api/mcp`;
|
|
113
|
+
}
|
|
114
|
+
export async function resolveAuth(options) {
|
|
115
|
+
const config = await loadConfig();
|
|
116
|
+
const explicitApiKey = options?.apiKey?.trim() || null;
|
|
117
|
+
const explicitAccessToken = options?.accessToken?.trim() || null;
|
|
118
|
+
const envApiKey = readEnvString("SALESTOUCH_API_KEY");
|
|
119
|
+
const envAccessToken = readEnvString("SALESTOUCH_ACCESS_TOKEN");
|
|
120
|
+
const configAccessToken = config.accessToken?.trim() || null;
|
|
121
|
+
const configApiKey = config.apiKey?.trim() || null;
|
|
122
|
+
const baseUrl = await resolveBaseUrl(options?.baseUrl ?? null);
|
|
123
|
+
if (explicitApiKey) {
|
|
124
|
+
return {
|
|
125
|
+
authMethod: "api_key",
|
|
126
|
+
credentialSource: "flag",
|
|
127
|
+
apiKey: explicitApiKey,
|
|
128
|
+
...baseUrl,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (envApiKey) {
|
|
132
|
+
return {
|
|
133
|
+
authMethod: "api_key",
|
|
134
|
+
credentialSource: "env",
|
|
135
|
+
apiKey: envApiKey,
|
|
136
|
+
...baseUrl,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (explicitAccessToken) {
|
|
140
|
+
return {
|
|
141
|
+
authMethod: "session",
|
|
142
|
+
credentialSource: "flag",
|
|
143
|
+
accessToken: explicitAccessToken,
|
|
144
|
+
...baseUrl,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (envAccessToken) {
|
|
148
|
+
return {
|
|
149
|
+
authMethod: "session",
|
|
150
|
+
credentialSource: "env",
|
|
151
|
+
accessToken: envAccessToken,
|
|
152
|
+
...baseUrl,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (configAccessToken) {
|
|
156
|
+
return {
|
|
157
|
+
authMethod: "session",
|
|
158
|
+
credentialSource: "config",
|
|
159
|
+
accessToken: configAccessToken,
|
|
160
|
+
...baseUrl,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (configApiKey) {
|
|
164
|
+
return {
|
|
165
|
+
authMethod: "api_key",
|
|
166
|
+
credentialSource: "config",
|
|
167
|
+
apiKey: configApiKey,
|
|
168
|
+
...baseUrl,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
throw new Error("Missing authentication. Use `salestouch auth login`, `salestouch auth use-key`, `--api-key`, SALESTOUCH_API_KEY, or SALESTOUCH_ACCESS_TOKEN.");
|
|
172
|
+
}
|
|
173
|
+
export async function persistAuth(auth) {
|
|
174
|
+
const current = await loadConfig();
|
|
175
|
+
await saveConfig({
|
|
176
|
+
...current,
|
|
177
|
+
baseUrl: auth.baseUrl,
|
|
178
|
+
apiKey: auth.authMethod === "api_key" ? auth.apiKey : undefined,
|
|
179
|
+
accessToken: auth.authMethod === "session" ? auth.accessToken : undefined,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
export async function clearStoredSession() {
|
|
183
|
+
const current = await loadConfig();
|
|
184
|
+
await saveConfig({
|
|
185
|
+
...current,
|
|
186
|
+
accessToken: undefined,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
export async function clearStoredApiKey() {
|
|
190
|
+
const current = await loadConfig();
|
|
191
|
+
await saveConfig({
|
|
192
|
+
...current,
|
|
193
|
+
apiKey: undefined,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
export async function clearStoredAuth() {
|
|
197
|
+
const current = await loadConfig();
|
|
198
|
+
await saveConfig({
|
|
199
|
+
...current,
|
|
200
|
+
apiKey: undefined,
|
|
201
|
+
accessToken: undefined,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async function readResponseJson(response) {
|
|
205
|
+
const text = await response.text();
|
|
206
|
+
if (!text) {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(text);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
throw new Error(`Invalid JSON response (${response.status}).`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function apiRequest(auth, pathName, init) {
|
|
217
|
+
const headers = new Headers(init?.headers);
|
|
218
|
+
headers.set("Accept", "application/json");
|
|
219
|
+
headers.set("Content-Type", "application/json");
|
|
220
|
+
headers.set("X-SalesTouch-Client", "cli");
|
|
221
|
+
headers.set("X-SalesTouch-Client-Version", CLI_VERSION);
|
|
222
|
+
if (auth.authMethod === "api_key") {
|
|
223
|
+
headers.set("X-API-Key", auth.apiKey);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
headers.set("Authorization", `Bearer ${auth.accessToken}`);
|
|
227
|
+
}
|
|
228
|
+
const response = await fetch(`${auth.baseUrl}${pathName}`, {
|
|
229
|
+
...init,
|
|
230
|
+
headers,
|
|
231
|
+
});
|
|
232
|
+
const payload = await readResponseJson(response);
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
const suffix = typeof payload?.error?.code === "string" ? ` (${payload.error.code})` : "";
|
|
235
|
+
throw new Error(`${payload?.error?.message ?? `SalesTouch API error ${response.status}`}${suffix}`);
|
|
236
|
+
}
|
|
237
|
+
return payload;
|
|
238
|
+
}
|
|
239
|
+
export function outputJson(value) {
|
|
240
|
+
console.log(JSON.stringify(value, null, 2));
|
|
241
|
+
}
|
|
242
|
+
export function printTable(rows) {
|
|
243
|
+
const widths = rows[0]?.map((_, index) => Math.max(...rows.map((row) => (row[index] ?? "").length)));
|
|
244
|
+
if (!widths) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
rows.forEach((row, rowIndex) => {
|
|
248
|
+
const line = row
|
|
249
|
+
.map((column, index) => column.padEnd(widths[index] ?? column.length))
|
|
250
|
+
.join(" ");
|
|
251
|
+
console.log(line);
|
|
252
|
+
if (rowIndex === 0) {
|
|
253
|
+
console.log(widths.map((width) => "-".repeat(width)).join(" "));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
export function printResourceStatus(resource) {
|
|
258
|
+
const payload = (resource.payload ?? {});
|
|
259
|
+
console.log(`Organization: ${String(payload.organization_id ?? "unknown")}`);
|
|
260
|
+
console.log(`User: ${String(payload.user_id ?? "unknown")}`);
|
|
261
|
+
console.log(`Plugin mode: ${String(payload.plugin_mode ?? "local")}`);
|
|
262
|
+
console.log(`LinkedIn: ${String(payload.connected_accounts ?? 0)} connected`);
|
|
263
|
+
}
|
|
264
|
+
export function describeAuthMethod(auth) {
|
|
265
|
+
return auth.authMethod === "session" ? "salestouch_login" : "api_key";
|
|
266
|
+
}
|
|
267
|
+
async function requestDeviceCode(baseUrl) {
|
|
268
|
+
const response = await fetch(`${baseUrl}/api/auth/device/code`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: {
|
|
271
|
+
Accept: "application/json",
|
|
272
|
+
"Content-Type": "application/json",
|
|
273
|
+
},
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
client_id: CLI_DEVICE_CLIENT_ID,
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
const payload = await readResponseJson(response);
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
const notFoundHint = response.status === 404
|
|
281
|
+
? ` Check the base URL (${baseUrl}) or pass --base-url / SALESTOUCH_API_BASE_URL.`
|
|
282
|
+
: "";
|
|
283
|
+
throw new Error(payload.error_description ??
|
|
284
|
+
payload.error ??
|
|
285
|
+
`Failed to start SalesTouch login (${response.status}).${notFoundHint}`);
|
|
286
|
+
}
|
|
287
|
+
return payload;
|
|
288
|
+
}
|
|
289
|
+
async function pollDeviceToken({ baseUrl, deviceCode, intervalSeconds, expiresInSeconds, }) {
|
|
290
|
+
const deadline = Date.now() + expiresInSeconds * 1000;
|
|
291
|
+
let pollIntervalMs = Math.max(intervalSeconds, 1) * 1000;
|
|
292
|
+
while (Date.now() < deadline) {
|
|
293
|
+
await sleep(pollIntervalMs);
|
|
294
|
+
const response = await fetch(`${baseUrl}/api/auth/device/token`, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: {
|
|
297
|
+
Accept: "application/json",
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
},
|
|
300
|
+
body: JSON.stringify({
|
|
301
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
302
|
+
device_code: deviceCode,
|
|
303
|
+
client_id: CLI_DEVICE_CLIENT_ID,
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
const payload = await readResponseJson(response);
|
|
307
|
+
if (response.ok && payload.access_token) {
|
|
308
|
+
return payload;
|
|
309
|
+
}
|
|
310
|
+
switch (payload.error) {
|
|
311
|
+
case "authorization_pending":
|
|
312
|
+
continue;
|
|
313
|
+
case "slow_down":
|
|
314
|
+
pollIntervalMs += 5000;
|
|
315
|
+
continue;
|
|
316
|
+
case "access_denied":
|
|
317
|
+
throw new Error(payload.error_description ?? "SalesTouch login was denied.");
|
|
318
|
+
case "expired_token":
|
|
319
|
+
throw new Error(payload.error_description ?? "SalesTouch login expired.");
|
|
320
|
+
default:
|
|
321
|
+
throw new Error(payload.error_description ??
|
|
322
|
+
payload.error ??
|
|
323
|
+
`Failed to complete SalesTouch login (${response.status}).`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw new Error("SalesTouch login expired before it was approved.");
|
|
327
|
+
}
|
|
328
|
+
async function openBrowser(url) {
|
|
329
|
+
const command = process.platform === "darwin"
|
|
330
|
+
? { bin: "open", args: [url] }
|
|
331
|
+
: process.platform === "win32"
|
|
332
|
+
? { bin: "cmd", args: ["/c", "start", "", url] }
|
|
333
|
+
: { bin: "xdg-open", args: [url] };
|
|
334
|
+
return new Promise((resolve) => {
|
|
335
|
+
try {
|
|
336
|
+
const child = spawn(command.bin, command.args, {
|
|
337
|
+
detached: true,
|
|
338
|
+
stdio: "ignore",
|
|
339
|
+
});
|
|
340
|
+
child.on("error", () => resolve(false));
|
|
341
|
+
child.unref();
|
|
342
|
+
resolve(true);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
resolve(false);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
export async function loginWithDeviceFlow(options) {
|
|
350
|
+
const baseUrl = await resolveBaseUrl(options?.baseUrl ?? null);
|
|
351
|
+
const deviceCode = await requestDeviceCode(baseUrl.baseUrl);
|
|
352
|
+
const opened = await openBrowser(deviceCode.verification_uri_complete);
|
|
353
|
+
if (!options?.quiet) {
|
|
354
|
+
console.log("Approve the SalesTouch CLI login in your browser.");
|
|
355
|
+
console.log(`Code: ${deviceCode.user_code}`);
|
|
356
|
+
console.log(`URL: ${deviceCode.verification_uri_complete}`);
|
|
357
|
+
if (!opened) {
|
|
358
|
+
console.log("A browser could not be opened automatically. Open the URL above manually.");
|
|
359
|
+
}
|
|
360
|
+
console.log("");
|
|
361
|
+
}
|
|
362
|
+
const token = await pollDeviceToken({
|
|
363
|
+
baseUrl: baseUrl.baseUrl,
|
|
364
|
+
deviceCode: deviceCode.device_code,
|
|
365
|
+
intervalSeconds: deviceCode.interval,
|
|
366
|
+
expiresInSeconds: deviceCode.expires_in,
|
|
367
|
+
});
|
|
368
|
+
if (!token.access_token) {
|
|
369
|
+
throw new Error("SalesTouch login completed without an access token.");
|
|
370
|
+
}
|
|
371
|
+
const auth = {
|
|
372
|
+
authMethod: "session",
|
|
373
|
+
credentialSource: "config",
|
|
374
|
+
accessToken: token.access_token,
|
|
375
|
+
...baseUrl,
|
|
376
|
+
};
|
|
377
|
+
await persistAuth(auth);
|
|
378
|
+
return {
|
|
379
|
+
auth,
|
|
380
|
+
userCode: deviceCode.user_code,
|
|
381
|
+
};
|
|
382
|
+
}
|
package/dist/index.d.ts
ADDED