@crmforall/connect 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/dist/commands/doctor.js +115 -0
- package/dist/commands/init.js +169 -0
- package/dist/commands/status.js +43 -0
- package/dist/contracts.js +38 -0
- package/dist/index.js +56 -0
- package/dist/prompts.js +60 -0
- package/package.json +36 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runDoctor = runDoctor;
|
|
4
|
+
const connector_1 = require("@crmforall/connector");
|
|
5
|
+
const db_1 = require("@crmforall/connector/db");
|
|
6
|
+
const contracts_1 = require("../contracts");
|
|
7
|
+
const DEFAULT_CRM_SCHEMA = "crmforall";
|
|
8
|
+
/** connect init 설정 우선, 없으면 CRMFORALL_DB_URL 환경변수에서 접속 정보를 얻는다 */
|
|
9
|
+
function resolveDbSettings() {
|
|
10
|
+
const config = (0, connector_1.loadConfig)();
|
|
11
|
+
if (config) {
|
|
12
|
+
return {
|
|
13
|
+
creds: {
|
|
14
|
+
host: config.db.host,
|
|
15
|
+
port: config.db.port,
|
|
16
|
+
database: config.db.database,
|
|
17
|
+
user: config.db.user,
|
|
18
|
+
password: config.db.password,
|
|
19
|
+
ssl: config.db.ssl,
|
|
20
|
+
},
|
|
21
|
+
crmSchema: config.crmSchema,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const dbUrl = process.env.CRMFORALL_DB_URL;
|
|
25
|
+
if (!dbUrl)
|
|
26
|
+
return null;
|
|
27
|
+
const url = new URL(dbUrl);
|
|
28
|
+
return {
|
|
29
|
+
creds: {
|
|
30
|
+
host: url.hostname,
|
|
31
|
+
port: Number(url.port || 5432),
|
|
32
|
+
database: url.pathname.replace(/^\//, ""),
|
|
33
|
+
user: decodeURIComponent(url.username),
|
|
34
|
+
password: process.env.CRMFORALL_DB_PASSWORD ?? decodeURIComponent(url.password),
|
|
35
|
+
ssl: url.searchParams.get("ssl") === "true",
|
|
36
|
+
},
|
|
37
|
+
crmSchema: process.env.CRMFORALL_CRM_SCHEMA ?? DEFAULT_CRM_SCHEMA,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Connection Readiness Gate 진단.
|
|
42
|
+
* 접속 정보는 connect init이 저장한 설정 또는 환경변수에서만 읽는다 —
|
|
43
|
+
* 비밀번호를 명령 인자로 받지 않는다.
|
|
44
|
+
*/
|
|
45
|
+
async function runDoctor() {
|
|
46
|
+
const checks = [];
|
|
47
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
48
|
+
checks.push({
|
|
49
|
+
name: "node_version",
|
|
50
|
+
passed: major >= 22,
|
|
51
|
+
detail: `node ${process.versions.node} (>=22 필요)`,
|
|
52
|
+
});
|
|
53
|
+
const config = (0, connector_1.loadConfig)();
|
|
54
|
+
const platformConfigured = Boolean(config?.platformUrl ?? process.env.CRMFORALL_PLATFORM_URL);
|
|
55
|
+
checks.push({
|
|
56
|
+
name: "platform_url_configured",
|
|
57
|
+
passed: platformConfigured,
|
|
58
|
+
detail: platformConfigured
|
|
59
|
+
? "플랫폼 URL 설정됨"
|
|
60
|
+
: "플랫폼 URL 미설정 — connect init이 설정합니다",
|
|
61
|
+
});
|
|
62
|
+
let failCode = null;
|
|
63
|
+
const settings = resolveDbSettings();
|
|
64
|
+
if (!settings) {
|
|
65
|
+
checks.push({
|
|
66
|
+
name: "db_readiness",
|
|
67
|
+
passed: false,
|
|
68
|
+
detail: "DB 접속 정보 없음 — connect init 후 다시 실행하세요",
|
|
69
|
+
});
|
|
70
|
+
failCode = contracts_1.ERROR_CODE.CRM_SCHEMA_NOT_READY;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
try {
|
|
74
|
+
const adapter = new db_1.PostgresAdapter(settings.creds);
|
|
75
|
+
await adapter.connect();
|
|
76
|
+
try {
|
|
77
|
+
const report = await adapter.checkReadiness({
|
|
78
|
+
crmSchema: settings.crmSchema,
|
|
79
|
+
});
|
|
80
|
+
for (const c of report.checks) {
|
|
81
|
+
checks.push({
|
|
82
|
+
name: c.name,
|
|
83
|
+
passed: c.passed,
|
|
84
|
+
detail: c.detail,
|
|
85
|
+
remediationSql: c.remediationSql,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (report.overprivileged)
|
|
89
|
+
failCode = contracts_1.ERROR_CODE.OVERPRIVILEGED;
|
|
90
|
+
else if (!report.ready)
|
|
91
|
+
failCode = contracts_1.ERROR_CODE.CRM_SCHEMA_NOT_READY;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await adapter.disconnect();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
const isAuth = /password|authentication/i.test(message);
|
|
100
|
+
checks.push({
|
|
101
|
+
name: "db_readiness",
|
|
102
|
+
passed: false,
|
|
103
|
+
detail: `DB 연결 실패: ${message}`,
|
|
104
|
+
});
|
|
105
|
+
failCode = isAuth ? contracts_1.ERROR_CODE.AUTH_FAILED : contracts_1.ERROR_CODE.NET_UNREACHABLE;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const allPassed = checks.every((c) => c.passed);
|
|
109
|
+
if (!allPassed) {
|
|
110
|
+
const result = (0, contracts_1.jsonError)("doctor", failCode ?? contracts_1.ERROR_CODE.CRM_SCHEMA_NOT_READY, "일부 검사가 통과하지 못했습니다.");
|
|
111
|
+
result.data = { checks };
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
return (0, contracts_1.jsonOk)("doctor", { checks });
|
|
115
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runInit = runInit;
|
|
4
|
+
const connector_1 = require("@crmforall/connector");
|
|
5
|
+
const db_1 = require("@crmforall/connector/db");
|
|
6
|
+
const contracts_1 = require("../contracts");
|
|
7
|
+
const prompts_1 = require("../prompts");
|
|
8
|
+
async function registerWithPlatform(platformUrl, installToken) {
|
|
9
|
+
let res;
|
|
10
|
+
try {
|
|
11
|
+
res = await fetch(new URL("/api/connections/register", platformUrl), {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "content-type": "application/json" },
|
|
14
|
+
body: JSON.stringify({ installToken, sourceType: "postgresql" }),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
return (0, contracts_1.jsonError)("init", contracts_1.ERROR_CODE.PLATFORM_UNREACHABLE, `플랫폼(${platformUrl})에 연결할 수 없습니다: ${err instanceof Error ? err.message : err}`);
|
|
19
|
+
}
|
|
20
|
+
const body = (await res.json().catch(() => null));
|
|
21
|
+
if (!res.ok || !body || body.error) {
|
|
22
|
+
const code = body?.error?.code;
|
|
23
|
+
return (0, contracts_1.jsonError)("init", code === "E_TOKEN_REUSED" || code === "E_TOKEN_EXPIRED"
|
|
24
|
+
? contracts_1.ERROR_CODE.TOKEN_EXPIRED
|
|
25
|
+
: contracts_1.ERROR_CODE.AUTH_FAILED, body?.error?.message ?? `커넥터 등록 실패 (HTTP ${res.status})`);
|
|
26
|
+
}
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
async function collectDbSettings(json) {
|
|
30
|
+
// 환경변수 우선 — AI 운전/컨테이너 경로 (시크릿이 대화·인자에 남지 않는다)
|
|
31
|
+
const dbUrl = process.env.CRMFORALL_DB_URL;
|
|
32
|
+
if (dbUrl) {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(dbUrl);
|
|
35
|
+
return {
|
|
36
|
+
sourceType: "postgresql",
|
|
37
|
+
host: url.hostname,
|
|
38
|
+
port: Number(url.port || 5432),
|
|
39
|
+
database: url.pathname.replace(/^\//, ""),
|
|
40
|
+
user: decodeURIComponent(url.username),
|
|
41
|
+
password: process.env.CRMFORALL_DB_PASSWORD ?? decodeURIComponent(url.password),
|
|
42
|
+
ssl: url.searchParams.get("ssl") === "true",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return (0, contracts_1.jsonError)("init", contracts_1.ERROR_CODE.NET_UNREACHABLE, "CRMFORALL_DB_URL 형식이 올바르지 않습니다.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (json || !(0, prompts_1.isInteractive)()) {
|
|
50
|
+
return (0, contracts_1.jsonError)("init", contracts_1.ERROR_CODE.AUTH_FAILED, "비대화형 실행에서는 CRMFORALL_DB_URL(+CRMFORALL_DB_PASSWORD) 환경변수가 필요합니다. 비밀번호를 명령 인자로 넘기지 마세요.");
|
|
51
|
+
}
|
|
52
|
+
console.log("\nDB 접속 정보 (읽기 전용 계정 + CRM 스키마 쓰기 권한 권장)");
|
|
53
|
+
const host = await (0, prompts_1.prompt)("호스트");
|
|
54
|
+
const port = Number(await (0, prompts_1.prompt)("포트", "5432"));
|
|
55
|
+
const database = await (0, prompts_1.prompt)("데이터베이스");
|
|
56
|
+
const user = await (0, prompts_1.prompt)("사용자");
|
|
57
|
+
const password = await (0, prompts_1.promptHidden)("비밀번호 (입력 비표시)");
|
|
58
|
+
const ssl = (await (0, prompts_1.prompt)("SSL 사용? (y/N)", "N")).toLowerCase() === "y";
|
|
59
|
+
return { sourceType: "postgresql", host, port, database, user, password, ssl };
|
|
60
|
+
}
|
|
61
|
+
async function runInit(opts) {
|
|
62
|
+
const existing = (0, connector_1.loadConfig)();
|
|
63
|
+
// --- 1. 자격증명 확보: 토큰 교환 또는 기존 설정 재사용 ---
|
|
64
|
+
let identity;
|
|
65
|
+
if (opts.token) {
|
|
66
|
+
if (opts.token.length < 16) {
|
|
67
|
+
return (0, contracts_1.jsonError)("init", contracts_1.ERROR_CODE.TOKEN_EXPIRED, "설치 토큰 형식이 올바르지 않습니다. 웹 콘솔의 데이터 연결 마법사에서 발급하세요.");
|
|
68
|
+
}
|
|
69
|
+
const platformUrl = opts.platformUrl ??
|
|
70
|
+
process.env.CRMFORALL_PLATFORM_URL ??
|
|
71
|
+
((0, prompts_1.isInteractive)() && !opts.json
|
|
72
|
+
? await (0, prompts_1.prompt)("플랫폼 URL", "https://app.crmforall.com")
|
|
73
|
+
: undefined);
|
|
74
|
+
if (!platformUrl) {
|
|
75
|
+
return (0, contracts_1.jsonError)("init", contracts_1.ERROR_CODE.PLATFORM_UNREACHABLE, "--platform-url 또는 CRMFORALL_PLATFORM_URL이 필요합니다.");
|
|
76
|
+
}
|
|
77
|
+
const registered = await registerWithPlatform(platformUrl, opts.token);
|
|
78
|
+
if ("ok" in registered)
|
|
79
|
+
return registered;
|
|
80
|
+
identity = {
|
|
81
|
+
platformUrl,
|
|
82
|
+
tenantId: registered.tenantId,
|
|
83
|
+
connectionId: registered.connectionId,
|
|
84
|
+
connectorId: registered.connectorId,
|
|
85
|
+
connectorSecret: registered.connectorSecret,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
else if (existing) {
|
|
89
|
+
identity = existing; // 멱등 재실행 — 등록 단계 생략
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
return (0, contracts_1.jsonError)("init", contracts_1.ERROR_CODE.TOKEN_EXPIRED, "설치 토큰이 없습니다. 웹 콘솔에서 발급한 뒤 connect init --token=<토큰>으로 실행하세요.");
|
|
93
|
+
}
|
|
94
|
+
// --- 2. DB 접속 정보 — 기존 설정 재사용, 환경변수로 갱신 가능 ---
|
|
95
|
+
let dbSettings;
|
|
96
|
+
if (existing && !process.env.CRMFORALL_DB_URL) {
|
|
97
|
+
dbSettings = existing.db;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const collected = await collectDbSettings(Boolean(opts.json));
|
|
101
|
+
if ("ok" in collected)
|
|
102
|
+
return collected;
|
|
103
|
+
dbSettings = collected;
|
|
104
|
+
}
|
|
105
|
+
const config = connector_1.connectorConfigSchema.parse({
|
|
106
|
+
...identity,
|
|
107
|
+
crmSchema: process.env.CRMFORALL_CRM_SCHEMA ?? existing?.crmSchema ?? "crmforall",
|
|
108
|
+
db: dbSettings,
|
|
109
|
+
mapping: existing?.mapping ?? null,
|
|
110
|
+
});
|
|
111
|
+
// --- 3~4. Readiness Gate + CRM 스키마 생성 (설치 단계 DDL) ---
|
|
112
|
+
const adapter = new db_1.PostgresAdapter({
|
|
113
|
+
host: config.db.host,
|
|
114
|
+
port: config.db.port,
|
|
115
|
+
database: config.db.database,
|
|
116
|
+
user: config.db.user,
|
|
117
|
+
password: config.db.password,
|
|
118
|
+
ssl: config.db.ssl,
|
|
119
|
+
});
|
|
120
|
+
try {
|
|
121
|
+
await adapter.connect();
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
+
return (0, contracts_1.jsonError)("init", /password|authentication/i.test(message)
|
|
126
|
+
? contracts_1.ERROR_CODE.AUTH_FAILED
|
|
127
|
+
: contracts_1.ERROR_CODE.NET_UNREACHABLE, `DB 연결 실패: ${message}`);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
let report = await adapter.checkReadiness({ crmSchema: config.crmSchema });
|
|
131
|
+
if (report.overprivileged) {
|
|
132
|
+
const remediation = report.checks
|
|
133
|
+
.map((c) => c.remediationSql)
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.join("\n");
|
|
136
|
+
const result = (0, contracts_1.jsonError)("init", contracts_1.ERROR_CODE.OVERPRIVILEGED, "과권한 계정이 감지되어 활성화를 차단합니다. 아래 권한 축소 SQL을 DBA에게 전달하세요.");
|
|
137
|
+
result.data = { remediationSql: remediation };
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
// CRM 스키마가 없으면 생성 (멱등 DDL — 설치 단계에만 허용)
|
|
141
|
+
const schemaMissing = report.checks.some((c) => c.name === "crm_schema_exists" && !c.passed);
|
|
142
|
+
if (schemaMissing || !report.ready) {
|
|
143
|
+
await adapter.ensureCrmSchema({ crmSchema: config.crmSchema });
|
|
144
|
+
report = await adapter.checkReadiness({ crmSchema: config.crmSchema });
|
|
145
|
+
}
|
|
146
|
+
// --- 5. 설정 저장 (시크릿 포함 — 0600) ---
|
|
147
|
+
(0, connector_1.saveConfig)(config);
|
|
148
|
+
if (!report.ready) {
|
|
149
|
+
const result = (0, contracts_1.jsonError)("init", contracts_1.ERROR_CODE.CRM_SCHEMA_NOT_READY, "설정은 저장했지만 일부 검사가 통과하지 못했습니다. connect doctor로 상세를 확인하세요.");
|
|
150
|
+
result.data = { checks: report.checks };
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
return (0, contracts_1.jsonOk)("init", {
|
|
154
|
+
configPath: (0, connector_1.defaultConfigPath)(),
|
|
155
|
+
connectionId: config.connectionId,
|
|
156
|
+
connectorId: config.connectorId,
|
|
157
|
+
crmSchema: config.crmSchema,
|
|
158
|
+
ready: true,
|
|
159
|
+
nextSteps: [
|
|
160
|
+
"connect doctor — 환경 진단 재확인",
|
|
161
|
+
"MCP 클라이언트에 crmforall-connector를 등록하세요",
|
|
162
|
+
"propose_field_mapping → validate_mapping(persist=true)으로 필드 매핑을 확정하세요",
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await adapter.disconnect().catch(() => { });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runStatus = runStatus;
|
|
4
|
+
const connector_1 = require("@crmforall/connector");
|
|
5
|
+
const contracts_1 = require("../contracts");
|
|
6
|
+
/** 연결 상태 요약 — 시크릿은 출력하지 않는다 */
|
|
7
|
+
async function runStatus() {
|
|
8
|
+
const config = (0, connector_1.loadConfig)();
|
|
9
|
+
if (!config) {
|
|
10
|
+
return (0, contracts_1.jsonOk)("status", {
|
|
11
|
+
connector: "not_configured",
|
|
12
|
+
version: "0.1.0",
|
|
13
|
+
message: "설정이 없습니다. connect init을 먼저 실행하세요.",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
let platformReachable = false;
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(new URL("/api/health", config.platformUrl), {
|
|
19
|
+
signal: AbortSignal.timeout(5_000),
|
|
20
|
+
});
|
|
21
|
+
platformReachable = res.ok;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
platformReachable = false;
|
|
25
|
+
}
|
|
26
|
+
return (0, contracts_1.jsonOk)("status", {
|
|
27
|
+
connector: "configured",
|
|
28
|
+
version: "0.1.0",
|
|
29
|
+
configPath: (0, connector_1.defaultConfigPath)(),
|
|
30
|
+
connectionId: config.connectionId,
|
|
31
|
+
connectorId: config.connectorId,
|
|
32
|
+
platformUrl: config.platformUrl,
|
|
33
|
+
platformReachable,
|
|
34
|
+
db: {
|
|
35
|
+
sourceType: config.db.sourceType,
|
|
36
|
+
host: config.db.host,
|
|
37
|
+
database: config.db.database,
|
|
38
|
+
user: config.db.user,
|
|
39
|
+
},
|
|
40
|
+
crmSchema: config.crmSchema,
|
|
41
|
+
mappingVersion: config.mapping?.version ?? null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ERROR_HINTS = exports.ERROR_CODE = void 0;
|
|
4
|
+
exports.jsonOk = jsonOk;
|
|
5
|
+
exports.jsonError = jsonError;
|
|
6
|
+
/**
|
|
7
|
+
* @crmforall/contracts 미러 (CLI에 필요한 부분만).
|
|
8
|
+
* TODO: contracts 패키지 npm 발행 후 의존성으로 교체.
|
|
9
|
+
* 원본: crmforall-platform/packages/contracts/src/errors.ts
|
|
10
|
+
*/
|
|
11
|
+
exports.ERROR_CODE = {
|
|
12
|
+
NET_UNREACHABLE: "E_NET_UNREACHABLE",
|
|
13
|
+
AUTH_FAILED: "E_AUTH_FAILED",
|
|
14
|
+
OVERPRIVILEGED: "E_OVERPRIVILEGED",
|
|
15
|
+
SCHEMA_MISSING_FIELD: "E_SCHEMA_MISSING_FIELD",
|
|
16
|
+
TOKEN_EXPIRED: "E_TOKEN_EXPIRED",
|
|
17
|
+
CRM_SCHEMA_NOT_READY: "E_CRM_SCHEMA_NOT_READY",
|
|
18
|
+
PLATFORM_UNREACHABLE: "E_PLATFORM_UNREACHABLE",
|
|
19
|
+
};
|
|
20
|
+
exports.ERROR_HINTS = {
|
|
21
|
+
[exports.ERROR_CODE.NET_UNREACHABLE]: "DB 호스트에 도달할 수 없습니다. 방화벽/보안그룹에서 DB 포트의 아웃바운드 허용 여부를 확인하세요.",
|
|
22
|
+
[exports.ERROR_CODE.AUTH_FAILED]: "DB 인증에 실패했습니다. 계정/비밀번호와 계정 잠금 여부를 확인하세요.",
|
|
23
|
+
[exports.ERROR_CODE.OVERPRIVILEGED]: "과권한 계정이 감지되었습니다. 출력된 권한 축소 SQL을 실행한 뒤 connect doctor를 다시 실행하세요.",
|
|
24
|
+
[exports.ERROR_CODE.SCHEMA_MISSING_FIELD]: "필수 표준 필드가 매핑되지 않았습니다. connect mapping propose를 실행하세요.",
|
|
25
|
+
[exports.ERROR_CODE.TOKEN_EXPIRED]: "토큰이 만료되었습니다. 웹 콘솔에서 재발급 받으세요.",
|
|
26
|
+
[exports.ERROR_CODE.CRM_SCHEMA_NOT_READY]: "CRM 전용 스키마가 없거나 마이그레이션이 필요합니다. connect init 또는 connect upgrade를 실행하세요.",
|
|
27
|
+
[exports.ERROR_CODE.PLATFORM_UNREACHABLE]: "crmforall 플랫폼에 연결할 수 없습니다. 네트워크와 설치 토큰 상태를 확인하세요.",
|
|
28
|
+
};
|
|
29
|
+
function jsonOk(command, data) {
|
|
30
|
+
return { ok: true, command, data };
|
|
31
|
+
}
|
|
32
|
+
function jsonError(command, code, message) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
command,
|
|
36
|
+
error: { code, message, hint: exports.ERROR_HINTS[code] },
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const init_1 = require("./commands/init");
|
|
6
|
+
const doctor_1 = require("./commands/doctor");
|
|
7
|
+
const status_1 = require("./commands/status");
|
|
8
|
+
const program = new commander_1.Command();
|
|
9
|
+
program
|
|
10
|
+
.name("connect")
|
|
11
|
+
.description("crmforall 커넥터 설치/진단 CLI. AI 도구(Claude Code 등)로 운전하려면 패키지의 AGENTS.md를 참조하세요.")
|
|
12
|
+
.version("0.1.0");
|
|
13
|
+
function emit(result, asJson) {
|
|
14
|
+
if (asJson) {
|
|
15
|
+
console.log(JSON.stringify(result, null, 2));
|
|
16
|
+
}
|
|
17
|
+
else if (result.ok) {
|
|
18
|
+
console.log(`✓ ${result.command} 완료`);
|
|
19
|
+
if (result.data)
|
|
20
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.error(`✗ ${result.command} 실패 [${result.error?.code}]`);
|
|
24
|
+
console.error(` ${result.error?.message}`);
|
|
25
|
+
console.error(` 해결: ${result.error?.hint}`);
|
|
26
|
+
}
|
|
27
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
28
|
+
}
|
|
29
|
+
program
|
|
30
|
+
.command("init")
|
|
31
|
+
.description("설치 마법사 — 웹 콘솔에서 발급한 토큰으로 커넥터를 설치한다 (멱등)")
|
|
32
|
+
.option("--token <token>", "설치 토큰")
|
|
33
|
+
.option("--platform-url <url>", "플랫폼 URL (기본: CRMFORALL_PLATFORM_URL)")
|
|
34
|
+
.option("--json", "기계가 읽을 수 있는 JSON 출력")
|
|
35
|
+
.action(async (opts) => {
|
|
36
|
+
emit(await (0, init_1.runInit)({
|
|
37
|
+
token: opts.token,
|
|
38
|
+
platformUrl: opts.platformUrl,
|
|
39
|
+
json: opts.json,
|
|
40
|
+
}), Boolean(opts.json));
|
|
41
|
+
});
|
|
42
|
+
program
|
|
43
|
+
.command("doctor")
|
|
44
|
+
.description("Connection Readiness Gate 진단 — 네트워크/권한/스키마 일괄 검사")
|
|
45
|
+
.option("--json", "기계가 읽을 수 있는 JSON 출력")
|
|
46
|
+
.action(async (opts) => {
|
|
47
|
+
emit(await (0, doctor_1.runDoctor)(), Boolean(opts.json));
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command("status")
|
|
51
|
+
.description("연결 상태, 버전, 대기 작업 확인")
|
|
52
|
+
.option("--json", "기계가 읽을 수 있는 JSON 출력")
|
|
53
|
+
.action(async (opts) => {
|
|
54
|
+
emit(await (0, status_1.runStatus)(), Boolean(opts.json));
|
|
55
|
+
});
|
|
56
|
+
program.parseAsync(process.argv);
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isInteractive = isInteractive;
|
|
4
|
+
exports.prompt = prompt;
|
|
5
|
+
exports.promptHidden = promptHidden;
|
|
6
|
+
const promises_1 = require("node:readline/promises");
|
|
7
|
+
/**
|
|
8
|
+
* 대화형 프롬프트.
|
|
9
|
+
* 보안 원칙: 비밀번호는 promptHidden(입력 비표시)으로만 받는다.
|
|
10
|
+
* 비대화형(--json, AI 운전)에서는 프롬프트 대신 환경변수를 요구한다 —
|
|
11
|
+
* 시크릿이 AI 대화·명령 인자·로그에 남지 않게 하기 위함이다.
|
|
12
|
+
*/
|
|
13
|
+
function isInteractive() {
|
|
14
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
15
|
+
}
|
|
16
|
+
async function prompt(question, defaultValue) {
|
|
17
|
+
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
18
|
+
try {
|
|
19
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
20
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
21
|
+
return answer || defaultValue || "";
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
rl.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const CTRL_C = "\u0003";
|
|
28
|
+
const CTRL_D = "\u0004";
|
|
29
|
+
const BACKSPACE = "\u007f";
|
|
30
|
+
function promptHidden(question) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const { stdin, stdout } = process;
|
|
33
|
+
stdout.write(`${question}: `);
|
|
34
|
+
stdin.resume();
|
|
35
|
+
stdin.setRawMode?.(true);
|
|
36
|
+
let value = "";
|
|
37
|
+
const onData = (chunk) => {
|
|
38
|
+
const ch = chunk.toString("utf8");
|
|
39
|
+
if (ch === "\n" || ch === "\r" || ch === CTRL_D) {
|
|
40
|
+
stdin.setRawMode?.(false);
|
|
41
|
+
stdin.pause();
|
|
42
|
+
stdin.off("data", onData);
|
|
43
|
+
stdout.write("\n");
|
|
44
|
+
resolve(value);
|
|
45
|
+
}
|
|
46
|
+
else if (ch === CTRL_C) {
|
|
47
|
+
stdin.setRawMode?.(false);
|
|
48
|
+
stdout.write("\n");
|
|
49
|
+
process.exit(130);
|
|
50
|
+
}
|
|
51
|
+
else if (ch === BACKSPACE || ch === "\b") {
|
|
52
|
+
value = value.slice(0, -1);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
value += ch;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
stdin.on("data", onData);
|
|
59
|
+
});
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crmforall/connect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "crmforall 커넥터 설치/진단 CLI — npx @crmforall/connect init --token=...",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"crmforall-connect": "./dist/index.js",
|
|
8
|
+
"connect": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@crmforall/connector": "workspace:*",
|
|
19
|
+
"commander": "^13.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"typescript": "^5.8.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"license": "UNLICENSED",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/HosungYou/crmforall-connector"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22"
|
|
35
|
+
}
|
|
36
|
+
}
|