@obov/mext 0.0.0-stage → 0.2.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/build-cli.d.ts +39 -0
- package/dist/build-cli.js +167 -0
- package/dist/build-cli.js.map +1 -0
- package/dist/build.d.ts +65 -0
- package/dist/build.js +252 -0
- package/dist/build.js.map +1 -0
- package/dist/cli/args.d.ts +107 -0
- package/dist/cli/args.js +621 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +13 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/build.d.ts +11 -0
- package/dist/cli/build.js +87 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/dev-lock.d.ts +23 -0
- package/dist/cli/dev-lock.js +68 -0
- package/dist/cli/dev-lock.js.map +1 -0
- package/dist/cli/dev-server.d.ts +20 -0
- package/dist/cli/dev-server.js +75 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/dev.d.ts +21 -0
- package/dist/cli/dev.js +565 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/help.d.ts +31 -0
- package/dist/cli/help.js +238 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/io.d.ts +26 -0
- package/dist/cli/io.js +74 -0
- package/dist/cli/io.js.map +1 -0
- package/dist/cli/login.d.ts +32 -0
- package/dist/cli/login.js +284 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/logout.d.ts +9 -0
- package/dist/cli/logout.js +105 -0
- package/dist/cli/logout.js.map +1 -0
- package/dist/cli/main.d.ts +19 -0
- package/dist/cli/main.js +77 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/mcp.d.ts +16 -0
- package/dist/cli/mcp.js +92 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/output.d.ts +78 -0
- package/dist/cli/output.js +229 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/remote.d.ts +17 -0
- package/dist/cli/remote.js +35 -0
- package/dist/cli/remote.js.map +1 -0
- package/dist/cli/run.d.ts +14 -0
- package/dist/cli/run.js +218 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +37 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/client/cache.d.ts +104 -0
- package/dist/client/cache.js +208 -0
- package/dist/client/cache.js.map +1 -0
- package/dist/client/main.d.ts +31 -0
- package/dist/client/main.js +101 -0
- package/dist/client/main.js.map +1 -0
- package/dist/client/manifest.d.ts +19 -0
- package/dist/client/manifest.js +68 -0
- package/dist/client/manifest.js.map +1 -0
- package/dist/client/runner.d.ts +52 -0
- package/dist/client/runner.js +563 -0
- package/dist/client/runner.js.map +1 -0
- package/dist/command.d.ts +75 -0
- package/dist/command.js +44 -0
- package/dist/command.js.map +1 -0
- package/dist/config.d.ts +111 -0
- package/dist/config.js +197 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.js +12 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.d.ts +97 -0
- package/dist/instrumentation.js +165 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/invoke.d.ts +64 -0
- package/dist/invoke.js +152 -0
- package/dist/invoke.js.map +1 -0
- package/dist/lookup.d.ts +9 -0
- package/dist/lookup.js +27 -0
- package/dist/lookup.js.map +1 -0
- package/dist/manifest.d.ts +68 -0
- package/dist/manifest.js +77 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mcp/constants.d.ts +9 -0
- package/dist/mcp/constants.js +10 -0
- package/dist/mcp/constants.js.map +1 -0
- package/dist/mcp/dispatch.d.ts +66 -0
- package/dist/mcp/dispatch.js +249 -0
- package/dist/mcp/dispatch.js.map +1 -0
- package/dist/mcp/help.d.ts +57 -0
- package/dist/mcp/help.js +136 -0
- package/dist/mcp/help.js.map +1 -0
- package/dist/mcp/http.d.ts +96 -0
- package/dist/mcp/http.js +368 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/install.d.ts +17 -0
- package/dist/mcp/install.js +36 -0
- package/dist/mcp/install.js.map +1 -0
- package/dist/mcp/mrtr.d.ts +82 -0
- package/dist/mcp/mrtr.js +130 -0
- package/dist/mcp/mrtr.js.map +1 -0
- package/dist/mcp/server.d.ts +63 -0
- package/dist/mcp/server.js +210 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +75 -0
- package/dist/mcp/stdio.js +222 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/principal.d.ts +16 -0
- package/dist/principal.js +24 -0
- package/dist/principal.js.map +1 -0
- package/dist/registry.d.ts +90 -0
- package/dist/registry.js +169 -0
- package/dist/registry.js.map +1 -0
- package/dist/remote/browser.d.ts +15 -0
- package/dist/remote/browser.js +47 -0
- package/dist/remote/browser.js.map +1 -0
- package/dist/remote/client.d.ts +113 -0
- package/dist/remote/client.js +312 -0
- package/dist/remote/client.js.map +1 -0
- package/dist/remote/credentials.d.ts +48 -0
- package/dist/remote/credentials.js +196 -0
- package/dist/remote/credentials.js.map +1 -0
- package/dist/remote/index.d.ts +8 -0
- package/dist/remote/index.js +5 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/remote/oauth.d.ts +186 -0
- package/dist/remote/oauth.js +507 -0
- package/dist/remote/oauth.js.map +1 -0
- package/dist/scan.d.ts +34 -0
- package/dist/scan.js +155 -0
- package/dist/scan.js.map +1 -0
- package/dist/start.d.ts +63 -0
- package/dist/start.js +269 -0
- package/dist/start.js.map +1 -0
- package/dist/suggest.d.ts +7 -0
- package/dist/suggest.js +45 -0
- package/dist/suggest.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -4
- package/README.md +0 -3
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createApp, loadCliErrorFormatter, loadConfig, resolveCommandsDir, resolveLocalPrincipal } from "../config.js";
|
|
2
|
+
import { MextError } from "../errors.js";
|
|
3
|
+
import { isSegment } from "../ids.js";
|
|
4
|
+
import { loadProjectInstrumentation } from "../instrumentation.js";
|
|
5
|
+
import { authorize, findCommand, invoke } from "../invoke.js";
|
|
6
|
+
import { help } from "../mcp/help.js";
|
|
7
|
+
import { resolveCommand } from "../registry.js";
|
|
8
|
+
import { loadRegistry } from "../scan.js";
|
|
9
|
+
import { analyzeInput, assertCliConvertible, coerceValue, parseArgs } from "./args.js";
|
|
10
|
+
import { cliPath, renderHelp } from "./help.js";
|
|
11
|
+
import { createLineReader, readAll } from "./io.js";
|
|
12
|
+
import { CliInterrupted, emitError, EXIT, errorJson, exitCodeOf, formatError, formatJson, formatProgress, formatResult, } from "./output.js";
|
|
13
|
+
import { remoteTarget, runRemoteCommand } from "./remote.js";
|
|
14
|
+
export const RUN_PROG = "mext run";
|
|
15
|
+
/**
|
|
16
|
+
* mext run <ns> <cmd> [flags] — 로컬 실행 (ctx.principal.kind = local, mext.config.ts 의 localPrincipal 로 변경 가능).
|
|
17
|
+
* mext run help [target] · mext run [<ns> [<cmd>]] --help · --json · --yes.
|
|
18
|
+
* --url <url> (또는 MEXT_URL) = 원격 실행 (cli/remote.ts)
|
|
19
|
+
*/
|
|
20
|
+
export async function runCommand(argv, io) {
|
|
21
|
+
const remote = remoteTarget(argv, io.env);
|
|
22
|
+
if (remote)
|
|
23
|
+
return runRemoteCommand(remote, io);
|
|
24
|
+
const prog = RUN_PROG;
|
|
25
|
+
const lead = { json: false, yes: false, help: false };
|
|
26
|
+
let i = 0;
|
|
27
|
+
for (; i < argv.length; i++) {
|
|
28
|
+
const tok = argv[i];
|
|
29
|
+
if (!tok.startsWith("-") || tok === "-")
|
|
30
|
+
break;
|
|
31
|
+
if (tok === "--json")
|
|
32
|
+
lead.json = true;
|
|
33
|
+
else if (tok === "--yes")
|
|
34
|
+
lead.yes = true;
|
|
35
|
+
else if (tok === "--help" || tok === "-h")
|
|
36
|
+
lead.help = true;
|
|
37
|
+
else {
|
|
38
|
+
return report(usageError(`unknown flag ${tok} before the command`), io, prog, argv.includes("--json"));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const rest = argv.slice(i);
|
|
42
|
+
const beforeDashDash = rest.indexOf("--") < 0 ? rest : rest.slice(0, rest.indexOf("--"));
|
|
43
|
+
const json = lead.json || beforeDashDash.includes("--json");
|
|
44
|
+
const lines = createLineReader(io);
|
|
45
|
+
let formatter;
|
|
46
|
+
try {
|
|
47
|
+
// instrumentation.ts register() = 설정 · 명령어 모듈 import 전 (프로세스당 1회)
|
|
48
|
+
const instrumentation = await loadProjectInstrumentation(io.cwd);
|
|
49
|
+
const config = await loadConfig(io.cwd);
|
|
50
|
+
formatter = await loadCliErrorFormatter(io.cwd, config);
|
|
51
|
+
const registry = await loadRegistry(resolveCommandsDir(io.cwd, config));
|
|
52
|
+
assertCliConvertible(registry);
|
|
53
|
+
// mext run help [target...]
|
|
54
|
+
if (rest[0] === "help") {
|
|
55
|
+
const targetTokens = rest.slice(1).filter((t) => !t.startsWith("-"));
|
|
56
|
+
const { id, consumed } = resolvePath(registry, targetTokens);
|
|
57
|
+
if (consumed < targetTokens.length)
|
|
58
|
+
throw unknownAt(registry, id, targetTokens.slice(consumed));
|
|
59
|
+
return printHelp(registry, id, io, prog, json, config.tools);
|
|
60
|
+
}
|
|
61
|
+
const { id, consumed } = resolvePath(registry, rest);
|
|
62
|
+
const entry = id === "" ? undefined : resolveCommand(registry, id);
|
|
63
|
+
const next = rest[consumed];
|
|
64
|
+
const acceptsPositional = entry !== undefined && entry.command.positional.length > 0;
|
|
65
|
+
if (registry.namespaces.has(id) && next !== undefined && !next.startsWith("-") && !acceptsPositional) {
|
|
66
|
+
throw unknownAt(registry, id, rest.slice(consumed));
|
|
67
|
+
}
|
|
68
|
+
if (entry === undefined && next !== undefined && !next.startsWith("-")) {
|
|
69
|
+
throw unknownAt(registry, id, rest.slice(consumed));
|
|
70
|
+
}
|
|
71
|
+
const help = lead.help || beforeDashDash.includes("--help") || beforeDashDash.includes("-h");
|
|
72
|
+
if (help)
|
|
73
|
+
return printHelp(registry, id, io, prog, json, config.tools);
|
|
74
|
+
if (!entry) {
|
|
75
|
+
// 네임스페이스 (루트 포함) 만 지정 = 명령어 없음
|
|
76
|
+
const error = new MextError("UNKNOWN_COMMAND", id === "" ? "No command given." : `"${cliPath(id)}" is a namespace. Specify a command.`, { command: id, suggested: [], usage: id === "" ? "help" : `help ${id}` });
|
|
77
|
+
if (json)
|
|
78
|
+
return report(error, io, prog, true, formatter);
|
|
79
|
+
const text = `${formatError(error, prog)}\n${renderHelp(registry, id, { prog })}`;
|
|
80
|
+
return emitError(io, { body: errorJson(error), text, exitCode: EXIT.usage, json: false }, formatter);
|
|
81
|
+
}
|
|
82
|
+
const principal = await resolveLocalPrincipal(config);
|
|
83
|
+
authorize(entry, principal); // 권한 → 입력 검증 순서 (invoke 와 같음)
|
|
84
|
+
const parsed = await parseArgs(entry, rest.slice(consumed), { readStdin: () => readAll(io.stdin) });
|
|
85
|
+
const yes = lead.yes || parsed.yes;
|
|
86
|
+
const app = await createApp(config);
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const onAbort = () => controller.abort(io.signal?.reason);
|
|
89
|
+
if (io.signal?.aborted)
|
|
90
|
+
onAbort();
|
|
91
|
+
io.signal?.addEventListener("abort", onAbort, { once: true });
|
|
92
|
+
try {
|
|
93
|
+
const result = await invoke(registry, entry.id, parsed.input, {
|
|
94
|
+
principal,
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
progress: (update) => io.stderr.write(formatProgress(update)),
|
|
97
|
+
ask: askHandler(io, lines, yes, controller.signal),
|
|
98
|
+
app,
|
|
99
|
+
instrumentation,
|
|
100
|
+
transport: "local",
|
|
101
|
+
});
|
|
102
|
+
if (controller.signal.aborted)
|
|
103
|
+
throw new CliInterrupted();
|
|
104
|
+
io.stdout.write(json ? formatJson(result.value) : formatResult(result.value));
|
|
105
|
+
return EXIT.ok;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (controller.signal.aborted)
|
|
109
|
+
throw new CliInterrupted();
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
io.signal?.removeEventListener("abort", onAbort);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return report(error, io, prog, json, formatter);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
lines.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** 앞쪽 경로 토큰 → 가장 긴 네임스페이스 · 명령어 (별칭 포함) ID */
|
|
124
|
+
export function resolvePath(registry, tokens) {
|
|
125
|
+
let id = "";
|
|
126
|
+
let consumed = 0;
|
|
127
|
+
for (const tok of tokens) {
|
|
128
|
+
if (!isSegment(tok))
|
|
129
|
+
break;
|
|
130
|
+
const candidate = id === "" ? tok : `${id}.${tok}`;
|
|
131
|
+
if (!registry.namespaces.has(candidate) && !resolveCommand(registry, candidate))
|
|
132
|
+
break;
|
|
133
|
+
id = candidate;
|
|
134
|
+
consumed++;
|
|
135
|
+
}
|
|
136
|
+
return { id, consumed };
|
|
137
|
+
}
|
|
138
|
+
/** 해석되지 않은 경로 토큰 → UNKNOWN_COMMAND (suggested · usage) */
|
|
139
|
+
function unknownAt(registry, id, tail) {
|
|
140
|
+
const segs = [];
|
|
141
|
+
for (const t of tail) {
|
|
142
|
+
if (t.startsWith("-") || (segs.length > 0 && !isSegment(t)))
|
|
143
|
+
break;
|
|
144
|
+
segs.push(t);
|
|
145
|
+
if (!isSegment(t))
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
const unknown = [id, ...segs].filter((s) => s !== "").join(".");
|
|
149
|
+
try {
|
|
150
|
+
findCommand(registry, unknown);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof MextError)
|
|
154
|
+
return error;
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return new MextError("UNKNOWN_COMMAND", `Unknown command "${unknown}".`, { command: unknown, suggested: [], usage: "help" });
|
|
158
|
+
}
|
|
159
|
+
/** --help --json = MCP help 의 structuredContent 와 같은 데이터 (mext mcp 의 tools 노출 기준) */
|
|
160
|
+
function printHelp(registry, id, io, prog, json, tools) {
|
|
161
|
+
const toolFor = tools === "split" ? (e) => (e.command.readOnly ? "query" : "mutate") : undefined;
|
|
162
|
+
io.stdout.write(json ? formatJson(help(registry, id, toolFor ? { toolFor } : {})) : renderHelp(registry, id, { prog }));
|
|
163
|
+
return EXIT.ok;
|
|
164
|
+
}
|
|
165
|
+
function report(error, io, prog, json, formatter) {
|
|
166
|
+
return emitError(io, { body: errorJson(error), text: formatError(error, prog), exitCode: exitCodeOf(error), json }, formatter);
|
|
167
|
+
}
|
|
168
|
+
function usageError(message) {
|
|
169
|
+
return new MextError("INVALID_ARGUMENT", message, {
|
|
170
|
+
command: "",
|
|
171
|
+
missing_fields: [],
|
|
172
|
+
issues: [{ path: "", code: "unknown_flag", message }],
|
|
173
|
+
expected_schema: {},
|
|
174
|
+
examples: [],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* ctx.ask. --yes = confirm 승인. TTY 없음 = 즉시 INPUT_UNAVAILABLE.
|
|
179
|
+
* TTY = stderr 프롬프트 + stdin 한 줄 (confirm = y/yes, input = 필드별 입력을 스키마 모양으로 변환)
|
|
180
|
+
*/
|
|
181
|
+
function askHandler(io, lines, yes, signal) {
|
|
182
|
+
return async (request) => {
|
|
183
|
+
if (request.kind === "confirm" && yes)
|
|
184
|
+
return true;
|
|
185
|
+
if (!io.isTTY) {
|
|
186
|
+
const hint = request.kind === "confirm" ? " Pass --yes to approve." : " Run it in an interactive terminal.";
|
|
187
|
+
throw new MextError("INPUT_UNAVAILABLE", `${request.command} asked "${request.message}", but stdin is not a terminal.${hint}`, { command: request.command });
|
|
188
|
+
}
|
|
189
|
+
if (request.kind === "confirm") {
|
|
190
|
+
const answer = await lines.question(`${request.message} [y/N] `, signal);
|
|
191
|
+
if (signal.aborted)
|
|
192
|
+
throw new CliInterrupted();
|
|
193
|
+
return /^y(es)?$/i.test((answer ?? "").trim());
|
|
194
|
+
}
|
|
195
|
+
io.stderr.write(`${request.message}\n`);
|
|
196
|
+
const { spec } = analyzeInput(request.schema, request.requestedSchema);
|
|
197
|
+
const out = {};
|
|
198
|
+
for (const f of spec.fields) {
|
|
199
|
+
const answer = await lines.question(` ${f.key}${f.required ? "" : " (optional)"}: `, signal);
|
|
200
|
+
if (signal.aborted)
|
|
201
|
+
throw new CliInterrupted();
|
|
202
|
+
if (answer === undefined || answer === "")
|
|
203
|
+
continue;
|
|
204
|
+
if (f.kind.type === "array") {
|
|
205
|
+
const kind = f.kind;
|
|
206
|
+
out[f.key] = answer.split(",").map((s) => {
|
|
207
|
+
const r = coerceValue(kind, s.trim());
|
|
208
|
+
return r.ok ? r.value : s;
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const r = coerceValue(f.kind, answer);
|
|
213
|
+
out[f.key] = r.ok ? r.value : answer;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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","sourcesContent":["import { createApp, loadCliErrorFormatter, loadConfig, resolveCommandsDir, resolveLocalPrincipal } from \"../config.ts\"\nimport { MextError } from \"../errors.ts\"\nimport { isSegment } from \"../ids.ts\"\nimport { loadProjectInstrumentation } from \"../instrumentation.ts\"\nimport { authorize, findCommand, invoke, type AskHandler } from \"../invoke.ts\"\nimport type { ToolExposure } from \"../mcp/dispatch.ts\"\nimport { help } from \"../mcp/help.ts\"\nimport { resolveCommand, type RegisteredCommand, type Registry } from \"../registry.ts\"\nimport { loadRegistry } from \"../scan.ts\"\nimport { analyzeInput, assertCliConvertible, coerceValue, parseArgs } from \"./args.ts\"\nimport { cliPath, renderHelp } from \"./help.ts\"\nimport { createLineReader, readAll, type CliIo, type LineReader } from \"./io.ts\"\nimport {\n CliInterrupted,\n emitError,\n EXIT,\n errorJson,\n exitCodeOf,\n formatError,\n formatJson,\n formatProgress,\n formatResult,\n type CliErrorFormatter,\n} from \"./output.ts\"\nimport { remoteTarget, runRemoteCommand } from \"./remote.ts\"\n\nexport const RUN_PROG = \"mext run\"\n\n/**\n * mext run <ns> <cmd> [flags] — 로컬 실행 (ctx.principal.kind = local, mext.config.ts 의 localPrincipal 로 변경 가능).\n * mext run help [target] · mext run [<ns> [<cmd>]] --help · --json · --yes.\n * --url <url> (또는 MEXT_URL) = 원격 실행 (cli/remote.ts)\n */\nexport async function runCommand(argv: readonly string[], io: CliIo): Promise<number> {\n const remote = remoteTarget(argv, io.env)\n if (remote) return runRemoteCommand(remote, io)\n const prog = RUN_PROG\n const lead = { json: false, yes: false, help: false }\n let i = 0\n for (; i < argv.length; i++) {\n const tok = argv[i]!\n if (!tok.startsWith(\"-\") || tok === \"-\") break\n if (tok === \"--json\") lead.json = true\n else if (tok === \"--yes\") lead.yes = true\n else if (tok === \"--help\" || tok === \"-h\") lead.help = true\n else {\n return report(usageError(`unknown flag ${tok} before the command`), io, prog, argv.includes(\"--json\"))\n }\n }\n const rest = argv.slice(i)\n const beforeDashDash = rest.indexOf(\"--\") < 0 ? rest : rest.slice(0, rest.indexOf(\"--\"))\n const json = lead.json || beforeDashDash.includes(\"--json\")\n\n const lines = createLineReader(io)\n let formatter: CliErrorFormatter | undefined\n try {\n // instrumentation.ts register() = 설정 · 명령어 모듈 import 전 (프로세스당 1회)\n const instrumentation = await loadProjectInstrumentation(io.cwd)\n const config = await loadConfig(io.cwd)\n formatter = await loadCliErrorFormatter(io.cwd, config)\n const registry = await loadRegistry(resolveCommandsDir(io.cwd, config))\n assertCliConvertible(registry)\n\n // mext run help [target...]\n if (rest[0] === \"help\") {\n const targetTokens = rest.slice(1).filter((t) => !t.startsWith(\"-\"))\n const { id, consumed } = resolvePath(registry, targetTokens)\n if (consumed < targetTokens.length) throw unknownAt(registry, id, targetTokens.slice(consumed))\n return printHelp(registry, id, io, prog, json, config.tools)\n }\n\n const { id, consumed } = resolvePath(registry, rest)\n const entry = id === \"\" ? undefined : resolveCommand(registry, id)\n const next = rest[consumed]\n const acceptsPositional = entry !== undefined && entry.command.positional.length > 0\n if (registry.namespaces.has(id) && next !== undefined && !next.startsWith(\"-\") && !acceptsPositional) {\n throw unknownAt(registry, id, rest.slice(consumed))\n }\n if (entry === undefined && next !== undefined && !next.startsWith(\"-\")) {\n throw unknownAt(registry, id, rest.slice(consumed))\n }\n\n const help = lead.help || beforeDashDash.includes(\"--help\") || beforeDashDash.includes(\"-h\")\n if (help) return printHelp(registry, id, io, prog, json, config.tools)\n\n if (!entry) {\n // 네임스페이스 (루트 포함) 만 지정 = 명령어 없음\n const error = new MextError(\n \"UNKNOWN_COMMAND\",\n id === \"\" ? \"No command given.\" : `\"${cliPath(id)}\" is a namespace. Specify a command.`,\n { command: id, suggested: [], usage: id === \"\" ? \"help\" : `help ${id}` },\n )\n if (json) return report(error, io, prog, true, formatter)\n const text = `${formatError(error, prog)}\\n${renderHelp(registry, id, { prog })}`\n return emitError(io, { body: errorJson(error), text, exitCode: EXIT.usage, json: false }, formatter)\n }\n\n const principal = await resolveLocalPrincipal(config)\n authorize(entry, principal) // 권한 → 입력 검증 순서 (invoke 와 같음)\n const parsed = await parseArgs(entry, rest.slice(consumed), { readStdin: () => readAll(io.stdin) })\n const yes = lead.yes || parsed.yes\n const app = await createApp(config)\n\n const controller = new AbortController()\n const onAbort = (): void => controller.abort(io.signal?.reason)\n if (io.signal?.aborted) onAbort()\n io.signal?.addEventListener(\"abort\", onAbort, { once: true })\n try {\n const result = await invoke(registry, entry.id, parsed.input, {\n principal,\n signal: controller.signal,\n progress: (update) => io.stderr.write(formatProgress(update)),\n ask: askHandler(io, lines, yes, controller.signal),\n app,\n instrumentation,\n transport: \"local\",\n })\n if (controller.signal.aborted) throw new CliInterrupted()\n io.stdout.write(json ? formatJson(result.value) : formatResult(result.value))\n return EXIT.ok\n } catch (error) {\n if (controller.signal.aborted) throw new CliInterrupted()\n throw error\n } finally {\n io.signal?.removeEventListener(\"abort\", onAbort)\n }\n } catch (error) {\n return report(error, io, prog, json, formatter)\n } finally {\n lines.close()\n }\n}\n\n/** 앞쪽 경로 토큰 → 가장 긴 네임스페이스 · 명령어 (별칭 포함) ID */\nexport function resolvePath(registry: Registry, tokens: readonly string[]): { id: string; consumed: number } {\n let id = \"\"\n let consumed = 0\n for (const tok of tokens) {\n if (!isSegment(tok)) break\n const candidate = id === \"\" ? tok : `${id}.${tok}`\n if (!registry.namespaces.has(candidate) && !resolveCommand(registry, candidate)) break\n id = candidate\n consumed++\n }\n return { id, consumed }\n}\n\n/** 해석되지 않은 경로 토큰 → UNKNOWN_COMMAND (suggested · usage) */\nfunction unknownAt(registry: Registry, id: string, tail: readonly string[]): MextError {\n const segs: string[] = []\n for (const t of tail) {\n if (t.startsWith(\"-\") || (segs.length > 0 && !isSegment(t))) break\n segs.push(t)\n if (!isSegment(t)) break\n }\n const unknown = [id, ...segs].filter((s) => s !== \"\").join(\".\")\n try {\n findCommand(registry, unknown)\n } catch (error) {\n if (error instanceof MextError) return error\n throw error\n }\n return new MextError(\"UNKNOWN_COMMAND\", `Unknown command \"${unknown}\".`, { command: unknown, suggested: [], usage: \"help\" })\n}\n\n/** --help --json = MCP help 의 structuredContent 와 같은 데이터 (mext mcp 의 tools 노출 기준) */\nfunction printHelp(registry: Registry, id: string, io: CliIo, prog: string, json: boolean, tools: ToolExposure | undefined): number {\n const toolFor = tools === \"split\" ? (e: RegisteredCommand) => (e.command.readOnly ? \"query\" : \"mutate\") : undefined\n io.stdout.write(json ? formatJson(help(registry, id, toolFor ? { toolFor } : {})) : renderHelp(registry, id, { prog }))\n return EXIT.ok\n}\n\nfunction report(error: unknown, io: CliIo, prog: string, json: boolean, formatter?: CliErrorFormatter): number {\n return emitError(io, { body: errorJson(error), text: formatError(error, prog), exitCode: exitCodeOf(error), json }, formatter)\n}\n\nfunction usageError(message: string): MextError<\"INVALID_ARGUMENT\"> {\n return new MextError(\"INVALID_ARGUMENT\", message, {\n command: \"\",\n missing_fields: [],\n issues: [{ path: \"\", code: \"unknown_flag\", message }],\n expected_schema: {},\n examples: [],\n })\n}\n\n\n/**\n * ctx.ask. --yes = confirm 승인. TTY 없음 = 즉시 INPUT_UNAVAILABLE.\n * TTY = stderr 프롬프트 + stdin 한 줄 (confirm = y/yes, input = 필드별 입력을 스키마 모양으로 변환)\n */\nfunction askHandler(io: CliIo, lines: LineReader, yes: boolean, signal: AbortSignal): AskHandler {\n return async (request) => {\n if (request.kind === \"confirm\" && yes) return true\n if (!io.isTTY) {\n const hint = request.kind === \"confirm\" ? \" Pass --yes to approve.\" : \" Run it in an interactive terminal.\"\n throw new MextError(\n \"INPUT_UNAVAILABLE\",\n `${request.command} asked \"${request.message}\", but stdin is not a terminal.${hint}`,\n { command: request.command },\n )\n }\n if (request.kind === \"confirm\") {\n const answer = await lines.question(`${request.message} [y/N] `, signal)\n if (signal.aborted) throw new CliInterrupted()\n return /^y(es)?$/i.test((answer ?? \"\").trim())\n }\n io.stderr.write(`${request.message}\\n`)\n const { spec } = analyzeInput(request.schema, request.requestedSchema)\n const out: Record<string, unknown> = {}\n for (const f of spec.fields) {\n const answer = await lines.question(` ${f.key}${f.required ? \"\" : \" (optional)\"}: `, signal)\n if (signal.aborted) throw new CliInterrupted()\n if (answer === undefined || answer === \"\") continue\n if (f.kind.type === \"array\") {\n const kind = f.kind\n out[f.key] = answer.split(\",\").map((s) => {\n const r = coerceValue(kind, s.trim())\n return r.ok ? r.value : s\n })\n continue\n }\n const r = coerceValue(f.kind, answer)\n out[f.key] = r.ok ? r.value : answer\n }\n return out\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CliIo } from "./io.ts";
|
|
2
|
+
export declare const START_PROG = "mext start";
|
|
3
|
+
/**
|
|
4
|
+
* mext start [--stdio] [--port <n>] [--host <h>] = startFromBuild(<cwd>/.mext) (node .mext/server.js 와 같음).
|
|
5
|
+
* .mext/ 만 읽는다 (소스 스캔 · 감시 · TypeScript 없음). 종료 = SIGINT · SIGTERM
|
|
6
|
+
*/
|
|
7
|
+
export declare function startCommand(argv: readonly string[], io: CliIo): Promise<number>;
|
|
8
|
+
export declare function startHelp(): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { BUILD_DIR } from "../build.js";
|
|
3
|
+
import { startFromBuild } from "../start.js";
|
|
4
|
+
import { table } from "./help.js";
|
|
5
|
+
import { EXIT } from "./output.js";
|
|
6
|
+
export const START_PROG = "mext start";
|
|
7
|
+
/**
|
|
8
|
+
* mext start [--stdio] [--port <n>] [--host <h>] = startFromBuild(<cwd>/.mext) (node .mext/server.js 와 같음).
|
|
9
|
+
* .mext/ 만 읽는다 (소스 스캔 · 감시 · TypeScript 없음). 종료 = SIGINT · SIGTERM
|
|
10
|
+
*/
|
|
11
|
+
export async function startCommand(argv, io) {
|
|
12
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
13
|
+
io.stdout.write(startHelp());
|
|
14
|
+
return EXIT.ok;
|
|
15
|
+
}
|
|
16
|
+
return startFromBuild(join(io.cwd, BUILD_DIR), { argv, env: io.env, stderr: io.stderr });
|
|
17
|
+
}
|
|
18
|
+
export function startHelp() {
|
|
19
|
+
const lines = [
|
|
20
|
+
"Production MCP server from .mext/ (run mext build first). No source scan, no file watching.",
|
|
21
|
+
"",
|
|
22
|
+
"Usage:",
|
|
23
|
+
` ${START_PROG} [--port <n>] [--host <h>] Streamable HTTP (POST /mcp)`,
|
|
24
|
+
` ${START_PROG} --stdio stdio`,
|
|
25
|
+
"",
|
|
26
|
+
"Flags:",
|
|
27
|
+
...table([
|
|
28
|
+
["--stdio", "Serve MCP over stdin/stdout"],
|
|
29
|
+
["--port <n>", "HTTP port (default: PORT, then mext.config.ts server.port, then 3100)"],
|
|
30
|
+
["--host <h>", "HTTP host (default: HOST, then mext.config.ts server.host, then 127.0.0.1)"],
|
|
31
|
+
]).map((l) => ` ${l}`),
|
|
32
|
+
"",
|
|
33
|
+
"Same as: node .mext/server.js [--stdio]",
|
|
34
|
+
];
|
|
35
|
+
return `${lines.join("\n")}\n`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=start.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start.js","sourceRoot":"","sources":["../../src/cli/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AAEjC,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAElC,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAA;AAEtC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAuB,EAAE,EAAS;IACnE,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAA;QAC5B,OAAO,IAAI,CAAC,EAAE,CAAA;IAChB,CAAC;IACD,OAAO,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAA;AAC1F,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,MAAM,KAAK,GAAG;QACZ,6FAA6F;QAC7F,EAAE;QACF,QAAQ;QACR,KAAK,UAAU,0DAA0D;QACzE,KAAK,UAAU,oCAAoC;QACnD,EAAE;QACF,QAAQ;QACR,GAAG,KAAK,CAAC;YACP,CAAC,SAAS,EAAE,6BAA6B,CAAC;YAC1C,CAAC,YAAY,EAAE,uEAAuE,CAAC;YACvF,CAAC,YAAY,EAAE,4EAA4E,CAAC;SAC7F,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,EAAE;QACF,yCAAyC;KAC1C,CAAA;IACD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAA;AAChC,CAAC","sourcesContent":["import { join } from \"node:path\"\nimport { BUILD_DIR } from \"../build.ts\"\nimport { startFromBuild } from \"../start.ts\"\nimport { table } from \"./help.ts\"\nimport type { CliIo } from \"./io.ts\"\nimport { EXIT } from \"./output.ts\"\n\nexport const START_PROG = \"mext start\"\n\n/**\n * mext start [--stdio] [--port <n>] [--host <h>] = startFromBuild(<cwd>/.mext) (node .mext/server.js 와 같음).\n * .mext/ 만 읽는다 (소스 스캔 · 감시 · TypeScript 없음). 종료 = SIGINT · SIGTERM\n */\nexport async function startCommand(argv: readonly string[], io: CliIo): Promise<number> {\n if (argv.includes(\"--help\") || argv.includes(\"-h\")) {\n io.stdout.write(startHelp())\n return EXIT.ok\n }\n return startFromBuild(join(io.cwd, BUILD_DIR), { argv, env: io.env, stderr: io.stderr })\n}\n\nexport function startHelp(): string {\n const lines = [\n \"Production MCP server from .mext/ (run mext build first). No source scan, no file watching.\",\n \"\",\n \"Usage:\",\n ` ${START_PROG} [--port <n>] [--host <h>] Streamable HTTP (POST /mcp)`,\n ` ${START_PROG} --stdio stdio`,\n \"\",\n \"Flags:\",\n ...table([\n [\"--stdio\", \"Serve MCP over stdin/stdout\"],\n [\"--port <n>\", \"HTTP port (default: PORT, then mext.config.ts server.port, then 3100)\"],\n [\"--host <h>\", \"HTTP host (default: HOST, then mext.config.ts server.host, then 127.0.0.1)\"],\n ]).map((l) => ` ${l}`),\n \"\",\n \"Same as: node .mext/server.js [--stdio]\",\n ]\n return `${lines.join(\"\\n\")}\\n`\n}\n"]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Help } from "../mcp/help.ts";
|
|
2
|
+
import { type RemoteTool } from "../remote/client.ts";
|
|
3
|
+
import { type ClientManifest } from "./manifest.ts";
|
|
4
|
+
/**
|
|
5
|
+
* 원격 스키마 캐시 · drift 감지 (§5 스키마 drift). 고객 CLI · mext run --url 공용 (번들 포함, Node 내장만 import).
|
|
6
|
+
*
|
|
7
|
+
* D4. 파일 = <cache dir>/<sha256(정규화 서버 URL) 앞 16 hex>.json, 쓰기 = 같은 폴더 임시 파일 → rename (원자적).
|
|
8
|
+
* cache dir = <BIN>_CACHE_DIR → $XDG_CACHE_HOME/<bin> → $HOME/.cache/<bin>. 셋 다 없음 = 캐시 없음.
|
|
9
|
+
* 내용 = SchemaCacheEntry. url · client (bin · version · mext 버전) 가 다르면 무효, 수명 = tools/list ttlMs.
|
|
10
|
+
*
|
|
11
|
+
* 이번 실행의 help 출처 (mode):
|
|
12
|
+
* cache 유효 캐시 (수명 안 · stale 아님). help = 캐시 → (해시 = 내장 manifest 면) 내장 manifest → 서버 (캐시에 추가)
|
|
13
|
+
* embedded 번들 형태 · 유효 캐시 없음 · 서버가 내장 manifest 와 다르다는 기록 없음. help = 내장 manifest
|
|
14
|
+
* server 그 외 (범용 · stale · 서버가 내장 manifest 와 다름). help = 서버 (tools/list + help), 캐시 새로 씀
|
|
15
|
+
* tools/call 결과 _meta 의 manifestHash ≠ 이번 help 의 해시 = stale 표시 (이번 결과는 그대로, 다음 실행이 server).
|
|
16
|
+
* 로컬 파싱 실패 = refetch() 로 server 전환 후 1회 재시도 (runner). 오프라인 = 남은 캐시 · 내장 manifest 로 help 만
|
|
17
|
+
*/
|
|
18
|
+
export declare const SCHEMA_CACHE_FORMAT = 1;
|
|
19
|
+
/** 캐시 무효화 단위. 하나라도 다르면 캐시 무시 */
|
|
20
|
+
export interface CacheClient {
|
|
21
|
+
/** 실행 파일 이름 (clientInfo.name) */
|
|
22
|
+
bin: string;
|
|
23
|
+
/** clientInfo.version (고객 CLI = cli.version, mext = mext 버전) */
|
|
24
|
+
version: string;
|
|
25
|
+
/** 클라이언트 런타임의 @obov/mext 버전 (help 해석 코드) */
|
|
26
|
+
mext: string;
|
|
27
|
+
}
|
|
28
|
+
/** help 결과 또는 UNKNOWN_COMMAND (경로 해석이 가장 긴 접두부터 시도하므로 없는 대상도 기록) */
|
|
29
|
+
export type CachedHelp = {
|
|
30
|
+
help: Help;
|
|
31
|
+
} | {
|
|
32
|
+
error: Record<string, unknown> & {
|
|
33
|
+
code: string;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export interface SchemaCacheEntry {
|
|
38
|
+
format: typeof SCHEMA_CACHE_FORMAT;
|
|
39
|
+
/** 정규화 서버 URL (파일 이름 해시 충돌 확인) */
|
|
40
|
+
url: string;
|
|
41
|
+
client: CacheClient;
|
|
42
|
+
/** help · tools 가 속한 서버 manifestHash. 없음 = 서버가 보내지 않음 */
|
|
43
|
+
manifestHash?: string;
|
|
44
|
+
/** tools/list 받은 시각 (epoch ms) */
|
|
45
|
+
fetchedAt: number;
|
|
46
|
+
/** tools/list ttlMs. 0 = 매 실행 재조회 (오프라인 help 용으로만 남김) */
|
|
47
|
+
ttlMs: number;
|
|
48
|
+
/** 서버 manifestHash 가 help 와 다름을 관찰. 다음 실행에서 서버 help 재조회 */
|
|
49
|
+
stale?: true;
|
|
50
|
+
/** tools/list 결과 */
|
|
51
|
+
tools: RemoteTool[];
|
|
52
|
+
/** help 대상 ("" = 루트) → 결과 */
|
|
53
|
+
help: Record<string, CachedHelp>;
|
|
54
|
+
}
|
|
55
|
+
/** <BIN>_CACHE_DIR → $XDG_CACHE_HOME/<bin> → $HOME/.cache/<bin> (USERPROFILE). 없으면 undefined (캐시 없음) */
|
|
56
|
+
export declare function schemaCacheDir(bin: string, env: Record<string, string | undefined>): string | undefined;
|
|
57
|
+
/** <dir>/<sha256(canonicalServerUrl(url)) 앞 16 hex>.json */
|
|
58
|
+
export declare function schemaCacheFile(dir: string, url: string): string;
|
|
59
|
+
/** 읽기 + 형식 · url · client 확인. 없음 · 깨짐 · 불일치 = undefined */
|
|
60
|
+
export declare function readSchemaCache(file: string, url: string, client: CacheClient): Promise<SchemaCacheEntry | undefined>;
|
|
61
|
+
/** 원자적 쓰기 (임시 파일 → rename). 실패는 호출자가 무시 (캐시는 최선) */
|
|
62
|
+
export declare function writeSchemaCache(file: string, entry: SchemaCacheEntry): Promise<void>;
|
|
63
|
+
export declare function isFresh(entry: SchemaCacheEntry, now: number): boolean;
|
|
64
|
+
export type SchemaMode = "cache" | "embedded" | "server";
|
|
65
|
+
export interface FetchedHelp {
|
|
66
|
+
value: CachedHelp;
|
|
67
|
+
/** 응답 _meta["dev.mext/manifestHash"] */
|
|
68
|
+
hash: string | undefined;
|
|
69
|
+
}
|
|
70
|
+
export interface SchemaOptions {
|
|
71
|
+
/** 캐시 파일. undefined = 캐시 없음 (읽기 · 쓰기 안 함) */
|
|
72
|
+
file: string | undefined;
|
|
73
|
+
url: string;
|
|
74
|
+
client: CacheClient;
|
|
75
|
+
/** 번들 형태의 내장 manifest + 빌드 시 manifestHash */
|
|
76
|
+
embedded?: {
|
|
77
|
+
manifest: ClientManifest;
|
|
78
|
+
hash: string;
|
|
79
|
+
};
|
|
80
|
+
/** 서버 help (tools/call help). UNKNOWN_COMMAND 는 value.error, 그 외 오류 = throw */
|
|
81
|
+
fetchHelp(target: string, tools: () => Promise<RemoteTool[]>): Promise<FetchedHelp>;
|
|
82
|
+
/** 서버 tools/list */
|
|
83
|
+
fetchTools(): Promise<{
|
|
84
|
+
tools: RemoteTool[];
|
|
85
|
+
ttlMs?: number;
|
|
86
|
+
}>;
|
|
87
|
+
/** 오프라인 help 안내 (stderr) */
|
|
88
|
+
warn(message: string): void;
|
|
89
|
+
now?: () => number;
|
|
90
|
+
}
|
|
91
|
+
export interface Schema {
|
|
92
|
+
readonly mode: SchemaMode;
|
|
93
|
+
/** HelpSource: 대상 → help. 없는 대상 = MextError UNKNOWN_COMMAND */
|
|
94
|
+
help(target: string): Promise<Help>;
|
|
95
|
+
/** 로컬 (cache · embedded) 파싱 실패 후 서버 help 로 전환. 이미 server = false (재시도 안 함) */
|
|
96
|
+
refetch(): boolean;
|
|
97
|
+
/** dispatcher 도구 목록 (유효 캐시 = 네트워크 없음) */
|
|
98
|
+
tools(): Promise<RemoteTool[]>;
|
|
99
|
+
/** tools/call 결과 _meta 의 manifestHash 관찰. 이번 help 의 해시와 다르면 stale */
|
|
100
|
+
observe(hash: string | undefined): void;
|
|
101
|
+
/** 바뀐 내용이 있으면 캐시 파일 쓰기 (실패 무시) */
|
|
102
|
+
save(): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
export declare function openSchema(options: SchemaOptions): Promise<Schema>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { MextError } from "../errors.js";
|
|
5
|
+
import { isRecord, RemoteTransportError } from "../remote/client.js";
|
|
6
|
+
import { canonicalServerUrl, envPrefix } from "../remote/credentials.js";
|
|
7
|
+
import { manifestHelp } from "./manifest.js";
|
|
8
|
+
/**
|
|
9
|
+
* 원격 스키마 캐시 · drift 감지 (§5 스키마 drift). 고객 CLI · mext run --url 공용 (번들 포함, Node 내장만 import).
|
|
10
|
+
*
|
|
11
|
+
* D4. 파일 = <cache dir>/<sha256(정규화 서버 URL) 앞 16 hex>.json, 쓰기 = 같은 폴더 임시 파일 → rename (원자적).
|
|
12
|
+
* cache dir = <BIN>_CACHE_DIR → $XDG_CACHE_HOME/<bin> → $HOME/.cache/<bin>. 셋 다 없음 = 캐시 없음.
|
|
13
|
+
* 내용 = SchemaCacheEntry. url · client (bin · version · mext 버전) 가 다르면 무효, 수명 = tools/list ttlMs.
|
|
14
|
+
*
|
|
15
|
+
* 이번 실행의 help 출처 (mode):
|
|
16
|
+
* cache 유효 캐시 (수명 안 · stale 아님). help = 캐시 → (해시 = 내장 manifest 면) 내장 manifest → 서버 (캐시에 추가)
|
|
17
|
+
* embedded 번들 형태 · 유효 캐시 없음 · 서버가 내장 manifest 와 다르다는 기록 없음. help = 내장 manifest
|
|
18
|
+
* server 그 외 (범용 · stale · 서버가 내장 manifest 와 다름). help = 서버 (tools/list + help), 캐시 새로 씀
|
|
19
|
+
* tools/call 결과 _meta 의 manifestHash ≠ 이번 help 의 해시 = stale 표시 (이번 결과는 그대로, 다음 실행이 server).
|
|
20
|
+
* 로컬 파싱 실패 = refetch() 로 server 전환 후 1회 재시도 (runner). 오프라인 = 남은 캐시 · 내장 manifest 로 help 만
|
|
21
|
+
*/
|
|
22
|
+
export const SCHEMA_CACHE_FORMAT = 1;
|
|
23
|
+
/** <BIN>_CACHE_DIR → $XDG_CACHE_HOME/<bin> → $HOME/.cache/<bin> (USERPROFILE). 없으면 undefined (캐시 없음) */
|
|
24
|
+
export function schemaCacheDir(bin, env) {
|
|
25
|
+
const forced = env[`${envPrefix(bin)}_CACHE_DIR`];
|
|
26
|
+
if (forced)
|
|
27
|
+
return forced;
|
|
28
|
+
if (env.XDG_CACHE_HOME)
|
|
29
|
+
return join(env.XDG_CACHE_HOME, bin);
|
|
30
|
+
const home = env.HOME || env.USERPROFILE;
|
|
31
|
+
return home ? join(home, ".cache", bin) : undefined;
|
|
32
|
+
}
|
|
33
|
+
/** <dir>/<sha256(canonicalServerUrl(url)) 앞 16 hex>.json */
|
|
34
|
+
export function schemaCacheFile(dir, url) {
|
|
35
|
+
return join(dir, `${createHash("sha256").update(canonicalServerUrl(url)).digest("hex").slice(0, 16)}.json`);
|
|
36
|
+
}
|
|
37
|
+
/** 읽기 + 형식 · url · client 확인. 없음 · 깨짐 · 불일치 = undefined */
|
|
38
|
+
export async function readSchemaCache(file, url, client) {
|
|
39
|
+
let value;
|
|
40
|
+
try {
|
|
41
|
+
value = JSON.parse(await readFile(file, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(value) || value.format !== SCHEMA_CACHE_FORMAT || value.url !== canonicalServerUrl(url))
|
|
47
|
+
return undefined;
|
|
48
|
+
const c = value.client;
|
|
49
|
+
if (!isRecord(c) || c.bin !== client.bin || c.version !== client.version || c.mext !== client.mext)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (typeof value.fetchedAt !== "number" || typeof value.ttlMs !== "number")
|
|
52
|
+
return undefined;
|
|
53
|
+
if (!Array.isArray(value.tools) || !isRecord(value.help))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (value.manifestHash !== undefined && typeof value.manifestHash !== "string")
|
|
56
|
+
return undefined;
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
/** 원자적 쓰기 (임시 파일 → rename). 실패는 호출자가 무시 (캐시는 최선) */
|
|
60
|
+
export async function writeSchemaCache(file, entry) {
|
|
61
|
+
const dir = join(file, "..");
|
|
62
|
+
await mkdir(dir, { recursive: true });
|
|
63
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
64
|
+
try {
|
|
65
|
+
await writeFile(tmp, `${JSON.stringify(entry)}\n`);
|
|
66
|
+
await rename(tmp, file);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
await rm(tmp, { force: true }).catch(() => { });
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function isFresh(entry, now) {
|
|
74
|
+
return entry.stale !== true && entry.ttlMs > 0 && now >= entry.fetchedAt && now < entry.fetchedAt + entry.ttlMs;
|
|
75
|
+
}
|
|
76
|
+
export async function openSchema(options) {
|
|
77
|
+
const clock = options.now ?? Date.now;
|
|
78
|
+
const started = clock();
|
|
79
|
+
const { embedded } = options;
|
|
80
|
+
const previous = options.file ? await readSchemaCache(options.file, options.url, options.client) : undefined;
|
|
81
|
+
const diverged = previous !== undefined &&
|
|
82
|
+
(previous.stale === true || (embedded !== undefined && previous.manifestHash !== undefined && previous.manifestHash !== embedded.hash));
|
|
83
|
+
let mode = previous !== undefined && isFresh(previous, started) ? "cache" : embedded && !diverged ? "embedded" : "server";
|
|
84
|
+
const cached = mode === "cache" ? previous : undefined;
|
|
85
|
+
let hash = cached ? cached.manifestHash : mode === "embedded" ? embedded.hash : undefined;
|
|
86
|
+
let help = cached ? { ...cached.help } : {};
|
|
87
|
+
let tools = cached?.tools;
|
|
88
|
+
let fetchedAt = cached?.fetchedAt ?? started;
|
|
89
|
+
let ttlMs = cached?.ttlMs ?? 0;
|
|
90
|
+
let stale = false;
|
|
91
|
+
let dirty = false;
|
|
92
|
+
let refetched = false;
|
|
93
|
+
let warned = false;
|
|
94
|
+
let pendingTools;
|
|
95
|
+
const loadTools = () => {
|
|
96
|
+
if (tools)
|
|
97
|
+
return Promise.resolve(tools);
|
|
98
|
+
return (pendingTools ??= options
|
|
99
|
+
.fetchTools()
|
|
100
|
+
.then((r) => {
|
|
101
|
+
tools = r.tools;
|
|
102
|
+
ttlMs = r.ttlMs ?? 0;
|
|
103
|
+
fetchedAt = clock();
|
|
104
|
+
dirty = true;
|
|
105
|
+
return r.tools;
|
|
106
|
+
})
|
|
107
|
+
.finally(() => {
|
|
108
|
+
pendingTools = undefined;
|
|
109
|
+
}));
|
|
110
|
+
};
|
|
111
|
+
const local = (target) => {
|
|
112
|
+
const hit = help[target];
|
|
113
|
+
if (hit)
|
|
114
|
+
return unpack(hit);
|
|
115
|
+
if (embedded && mode !== "server" && hash === embedded.hash)
|
|
116
|
+
return manifestHelp(embedded.manifest, target);
|
|
117
|
+
return undefined;
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
get mode() {
|
|
121
|
+
return mode;
|
|
122
|
+
},
|
|
123
|
+
async help(target) {
|
|
124
|
+
const found = local(target);
|
|
125
|
+
if (found)
|
|
126
|
+
return found;
|
|
127
|
+
let fetched;
|
|
128
|
+
try {
|
|
129
|
+
fetched = await options.fetchHelp(target, loadTools);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// 오프라인: 남은 캐시 (수명 지남 · stale) → 내장 manifest. refetch 중이면 runner 가 원래 로컬 오류를 낸다
|
|
133
|
+
if (error instanceof RemoteTransportError && !refetched) {
|
|
134
|
+
const old = previous?.help[target];
|
|
135
|
+
if (old || embedded) {
|
|
136
|
+
if (!warned)
|
|
137
|
+
options.warn(`warning: ${error.message} Showing ${old ? "cached" : "built-in"} help.\n`);
|
|
138
|
+
warned = true;
|
|
139
|
+
return old ? unpack(old) : manifestHelp(embedded.manifest, target);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
if (fetched.hash !== undefined) {
|
|
145
|
+
if (hash === undefined)
|
|
146
|
+
hash = fetched.hash;
|
|
147
|
+
else if (fetched.hash !== hash) {
|
|
148
|
+
if (mode === "server") {
|
|
149
|
+
// 실행 중 서버 교체: 이전 응답 버림
|
|
150
|
+
help = {};
|
|
151
|
+
hash = fetched.hash;
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
stale = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
help[target] = fetched.value;
|
|
158
|
+
dirty = true;
|
|
159
|
+
return unpack(fetched.value);
|
|
160
|
+
},
|
|
161
|
+
refetch() {
|
|
162
|
+
if (mode === "server")
|
|
163
|
+
return false;
|
|
164
|
+
mode = "server";
|
|
165
|
+
refetched = true;
|
|
166
|
+
help = {};
|
|
167
|
+
hash = undefined;
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
170
|
+
tools: loadTools,
|
|
171
|
+
observe(observed) {
|
|
172
|
+
if (observed === undefined)
|
|
173
|
+
return;
|
|
174
|
+
if (hash === undefined) {
|
|
175
|
+
hash = observed;
|
|
176
|
+
dirty = true;
|
|
177
|
+
}
|
|
178
|
+
else if (observed !== hash) {
|
|
179
|
+
hash = observed;
|
|
180
|
+
stale = true;
|
|
181
|
+
dirty = true;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
async save() {
|
|
185
|
+
if (!options.file || !dirty || !tools)
|
|
186
|
+
return;
|
|
187
|
+
const entry = {
|
|
188
|
+
format: SCHEMA_CACHE_FORMAT,
|
|
189
|
+
url: canonicalServerUrl(options.url),
|
|
190
|
+
client: options.client,
|
|
191
|
+
...(hash !== undefined ? { manifestHash: hash } : {}),
|
|
192
|
+
fetchedAt,
|
|
193
|
+
ttlMs,
|
|
194
|
+
...(stale ? { stale: true } : {}),
|
|
195
|
+
tools,
|
|
196
|
+
help,
|
|
197
|
+
};
|
|
198
|
+
await writeSchemaCache(options.file, entry).catch(() => { });
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function unpack(cached) {
|
|
203
|
+
if ("help" in cached)
|
|
204
|
+
return cached.help;
|
|
205
|
+
const { code, message, ...details } = cached.error;
|
|
206
|
+
throw new MextError(code, message, details);
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=cache.js.map
|