@ait-co/devtools 0.1.81 → 0.1.85
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.en.md +1 -1
- package/README.md +1 -1
- package/dist/{chii-relay-DSVG4Ui1.js → chii-relay-BASitNMw.js} +1 -1
- package/dist/{chii-relay-DSVG4Ui1.js.map → chii-relay-BASitNMw.js.map} +1 -1
- package/dist/{chii-relay-BcnVJBqm.cjs → chii-relay-BzUf0LH3.cjs} +1 -1
- package/dist/{chii-relay-BcnVJBqm.cjs.map → chii-relay-BzUf0LH3.cjs.map} +1 -1
- package/dist/{deeplink-B-94XmWA.js → deeplink-BpO9qc-D.js} +5 -3
- package/dist/{deeplink-BLU2_hg6.cjs.map → deeplink-BpO9qc-D.js.map} +1 -1
- package/dist/{deeplink-CYqDwVYs.js → deeplink-D1HXJ2YG.js} +5 -3
- package/dist/{deeplink-CU6opogq.cjs.map → deeplink-D1HXJ2YG.js.map} +1 -1
- package/dist/{deeplink-BLU2_hg6.cjs → deeplink-DCScMYcp.cjs} +5 -3
- package/dist/deeplink-DCScMYcp.cjs.map +1 -0
- package/dist/{deeplink-CU6opogq.cjs → deeplink-DDOe0FQl.cjs} +5 -3
- package/dist/deeplink-DDOe0FQl.cjs.map +1 -0
- package/dist/{devtools-opener-Bp671YXu.cjs → devtools-opener-BDY0w3_0.cjs} +11 -5
- package/dist/devtools-opener-BDY0w3_0.cjs.map +1 -0
- package/dist/{devtools-opener-D84kZFtR.js → devtools-opener-BTl5A6Cd.js} +11 -5
- package/dist/devtools-opener-BTl5A6Cd.js.map +1 -0
- package/dist/{devtools-opener-BbUXBzgA.js → devtools-opener-XpwL3fZ9.js} +22 -6
- package/dist/devtools-opener-XpwL3fZ9.js.map +1 -0
- package/dist/{devtools-opener-h6A-UjzC.cjs → devtools-opener-mDgeg_MX.cjs} +11 -5
- package/dist/devtools-opener-mDgeg_MX.cjs.map +1 -0
- package/dist/machine-state-Chg_6SPq.js +188 -0
- package/dist/machine-state-Chg_6SPq.js.map +1 -0
- package/dist/machine-state-DOUweFsJ.cjs +216 -0
- package/dist/machine-state-DOUweFsJ.cjs.map +1 -0
- package/dist/mcp/cli.js +112 -54
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +6 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +109 -8
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-DJ5K3Odk.js → qr-http-server-Buorblrx.js} +73 -23
- package/dist/qr-http-server-Buorblrx.js.map +1 -0
- package/dist/{qr-http-server-Dkx2-pKF.cjs → qr-http-server-BvAtX9Lc.cjs} +73 -23
- package/dist/qr-http-server-BvAtX9Lc.cjs.map +1 -0
- package/dist/{qr-http-server-DkOFfZsR.js → qr-http-server-BxjrJr9t.js} +73 -23
- package/dist/qr-http-server-BxjrJr9t.js.map +1 -0
- package/dist/{qr-http-server-MIUHaiYw.cjs → qr-http-server-CAUyOrCm.cjs} +73 -23
- package/dist/qr-http-server-CAUyOrCm.cjs.map +1 -0
- package/dist/{relay-secret-store-CsCOfpWt.cjs → relay-secret-store-B5WAozDv.cjs} +2 -2
- package/dist/{relay-secret-store-CsCOfpWt.cjs.map → relay-secret-store-B5WAozDv.cjs.map} +1 -1
- package/dist/{relay-secret-store-6pPzLkUO.js → relay-secret-store-BvNWdSjV.js} +2 -2
- package/dist/{relay-secret-store-6pPzLkUO.js.map → relay-secret-store-BvNWdSjV.js.map} +1 -1
- package/dist/{relay-url-store-DH8-VUFc.js → relay-url-store-1CXVqNDL.js} +2 -2
- package/dist/{relay-url-store-DH8-VUFc.js.map → relay-url-store-1CXVqNDL.js.map} +1 -1
- package/dist/{relay-url-store-BiEK9BN1.cjs → relay-url-store-D2lX9POP.cjs} +2 -2
- package/dist/{relay-url-store-BiEK9BN1.cjs.map → relay-url-store-D2lX9POP.cjs.map} +1 -1
- package/dist/{totp-DYdP9N3o.js → totp-CauHjkdE.js} +1 -1
- package/dist/{totp-DYdP9N3o.js.map → totp-CauHjkdE.js.map} +1 -1
- package/dist/{totp-CNw0w89F.cjs → totp-D9fjaVak.cjs} +1 -1
- package/dist/{totp-CNw0w89F.cjs.map → totp-D9fjaVak.cjs.map} +1 -1
- package/dist/{tunnel-C-AFdAVL.cjs → tunnel-CfT31xho.cjs} +6 -6
- package/dist/{tunnel-C-AFdAVL.cjs.map → tunnel-CfT31xho.cjs.map} +1 -1
- package/dist/{tunnel-BTlq1mmH.js → tunnel-DPwJBn1u.js} +6 -6
- package/dist/{tunnel-BTlq1mmH.js.map → tunnel-DPwJBn1u.js.map} +1 -1
- package/dist/unplugin/index.cjs +90 -5
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +90 -5
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/tunnel.cjs +4 -4
- package/dist/unplugin/tunnel.js +4 -4
- package/package.json +1 -1
- package/dist/deeplink-B-94XmWA.js.map +0 -1
- package/dist/deeplink-CYqDwVYs.js.map +0 -1
- package/dist/devtools-opener-BbUXBzgA.js.map +0 -1
- package/dist/devtools-opener-Bp671YXu.cjs.map +0 -1
- package/dist/devtools-opener-D84kZFtR.js.map +0 -1
- package/dist/devtools-opener-h6A-UjzC.cjs.map +0 -1
- package/dist/qr-http-server-DJ5K3Odk.js.map +0 -1
- package/dist/qr-http-server-DkOFfZsR.js.map +0 -1
- package/dist/qr-http-server-Dkx2-pKF.cjs.map +0 -1
- package/dist/qr-http-server-MIUHaiYw.cjs.map +0 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
let node_os = require("node:os");
|
|
14
|
+
let node_path = require("node:path");
|
|
15
|
+
//#region src/telemetry/machine-state.ts
|
|
16
|
+
/**
|
|
17
|
+
* Machine-level telemetry consent store — #542
|
|
18
|
+
*
|
|
19
|
+
* Persists consent to `~/.ait-devtools/telemetry.json` (Node.js side only).
|
|
20
|
+
* This eliminates repeated consent prompts when the dev origin rotates
|
|
21
|
+
* (quick-tunnel host changes per session, localhost port varies).
|
|
22
|
+
*
|
|
23
|
+
* Design invariants:
|
|
24
|
+
* - localStorage keys in src/telemetry/state.ts are LOCKED — this module does
|
|
25
|
+
* NOT replace them. It adds a machine-level overlay that takes precedence
|
|
26
|
+
* when a dev server is running.
|
|
27
|
+
* - Tier 0 / Tier 1 consent semantics are unchanged — only the storage
|
|
28
|
+
* location and lifetime change.
|
|
29
|
+
* - No PII. The file stores: consent enum + decided_at ISO timestamp +
|
|
30
|
+
* policy_version + anon_id (pseudonymous UUID).
|
|
31
|
+
* - anon_id is promoted to machine level so the same identity is used across
|
|
32
|
+
* all dev origins (prevents counting the same developer multiple times).
|
|
33
|
+
*
|
|
34
|
+
* Node.js-only: imported exclusively from the unplugin (server-side).
|
|
35
|
+
* Never imported from panel/browser code.
|
|
36
|
+
*/
|
|
37
|
+
var machine_state_exports = /* @__PURE__ */ __exportAll({
|
|
38
|
+
ensureMachineConsent: () => ensureMachineConsent,
|
|
39
|
+
machineStateDir: () => machineStateDir,
|
|
40
|
+
machineStateFile: () => machineStateFile,
|
|
41
|
+
promptTtyConsent: () => promptTtyConsent,
|
|
42
|
+
readMachineState: () => readMachineState,
|
|
43
|
+
writeMachineState: () => writeMachineState
|
|
44
|
+
});
|
|
45
|
+
/** Resolved directory: `~/.ait-devtools`. */
|
|
46
|
+
function machineStateDir(homeOverride) {
|
|
47
|
+
return (0, node_path.join)(homeOverride ?? (0, node_os.homedir)(), ".ait-devtools");
|
|
48
|
+
}
|
|
49
|
+
/** Resolved file: `~/.ait-devtools/telemetry.json`. */
|
|
50
|
+
function machineStateFile(homeOverride) {
|
|
51
|
+
return (0, node_path.join)(machineStateDir(homeOverride), "telemetry.json");
|
|
52
|
+
}
|
|
53
|
+
function isoNow() {
|
|
54
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
55
|
+
}
|
|
56
|
+
function newUUID() {
|
|
57
|
+
return crypto.randomUUID();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns a minimal MachineStateFsDep using real `node:fs` synchronous APIs.
|
|
61
|
+
* Resolved lazily to avoid pulling node:fs into non-Node environments.
|
|
62
|
+
*/
|
|
63
|
+
async function realFs() {
|
|
64
|
+
const { existsSync, mkdirSync, readFileSync, writeFileSync } = await import("node:fs");
|
|
65
|
+
return {
|
|
66
|
+
existsSync,
|
|
67
|
+
mkdirSync,
|
|
68
|
+
readFileSync,
|
|
69
|
+
writeFileSync
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reads the machine-level telemetry state.
|
|
74
|
+
* Returns `null` when the file is absent or cannot be parsed.
|
|
75
|
+
*
|
|
76
|
+
* NODE-ONLY. Never call from browser/panel code.
|
|
77
|
+
*/
|
|
78
|
+
async function readMachineState(deps) {
|
|
79
|
+
const fs = deps?.fs ?? await realFs();
|
|
80
|
+
const filePath = machineStateFile(deps?.homeDir);
|
|
81
|
+
if (!fs.existsSync(filePath)) return null;
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
if (parsed !== null && typeof parsed === "object" && "consent" in parsed && typeof parsed.consent === "string" && [
|
|
91
|
+
"granted",
|
|
92
|
+
"denied",
|
|
93
|
+
"undecided"
|
|
94
|
+
].includes(parsed.consent)) return parsed;
|
|
95
|
+
return null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Writes (or patches) the machine-level telemetry state file.
|
|
102
|
+
* Creates `~/.ait-devtools/` if it does not yet exist.
|
|
103
|
+
*
|
|
104
|
+
* NODE-ONLY. Never call from browser/panel code.
|
|
105
|
+
*/
|
|
106
|
+
async function writeMachineState(patch, deps) {
|
|
107
|
+
const fs = deps?.fs ?? await realFs();
|
|
108
|
+
const dir = machineStateDir(deps?.homeDir);
|
|
109
|
+
const filePath = machineStateFile(deps?.homeDir);
|
|
110
|
+
const now = deps?.now ?? isoNow;
|
|
111
|
+
const uuid = deps?.randomUUID ?? newUUID;
|
|
112
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
let existing = null;
|
|
114
|
+
if (fs.existsSync(filePath)) try {
|
|
115
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
116
|
+
existing = JSON.parse(raw);
|
|
117
|
+
} catch {}
|
|
118
|
+
const next = {
|
|
119
|
+
...existing ?? {
|
|
120
|
+
consent: "undecided",
|
|
121
|
+
decided_at: now(),
|
|
122
|
+
policy_version: "",
|
|
123
|
+
anon_id: null
|
|
124
|
+
},
|
|
125
|
+
...patch,
|
|
126
|
+
anon_id: existing?.anon_id ?? patch.anon_id ?? uuid(),
|
|
127
|
+
decided_at: now()
|
|
128
|
+
};
|
|
129
|
+
fs.writeFileSync(filePath, JSON.stringify(next, null, 2), { mode: 384 });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Prompts the user in the terminal (stdin/stdout TTY) to accept or deny
|
|
133
|
+
* telemetry consent — called once per machine on the first `pnpm dev` run.
|
|
134
|
+
*
|
|
135
|
+
* Returns the chosen `ConsentState`, or `'undecided'` when:
|
|
136
|
+
* - The TTY is not interactive (CI, headless, piped stdin).
|
|
137
|
+
* - The prompt times out.
|
|
138
|
+
* - An error occurs.
|
|
139
|
+
*
|
|
140
|
+
* NODE-ONLY. Called from the unplugin `configureServer` hook.
|
|
141
|
+
*/
|
|
142
|
+
async function promptTtyConsent() {
|
|
143
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return "undecided";
|
|
144
|
+
const { createInterface } = await import("node:readline");
|
|
145
|
+
const rl = createInterface({
|
|
146
|
+
input: process.stdin,
|
|
147
|
+
output: process.stdout
|
|
148
|
+
});
|
|
149
|
+
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
150
|
+
process.stdout.write("\n[@ait-co/devtools] 익명 사용 통계를 보내도 될까요?\n 버전·날짜만 수집하고 PII는 없습니다. 언제든 Environment 탭에서 끌 수 있어요.\n 자세히: https://docs.aitc.dev/privacy\n (y) 네, 보낼게요 (n) 아니요 (무응답 시 보류 — 다음 기동 때 다시 물어요)\n");
|
|
151
|
+
let answer = "";
|
|
152
|
+
try {
|
|
153
|
+
answer = await Promise.race([question(" > "), new Promise((resolve) => setTimeout(() => resolve(""), 3e4))]);
|
|
154
|
+
} catch {} finally {
|
|
155
|
+
rl.close();
|
|
156
|
+
}
|
|
157
|
+
const normalized = answer.trim().toLowerCase();
|
|
158
|
+
if (normalized === "y" || normalized === "yes" || normalized === "네" || normalized === "ㅇ") return "granted";
|
|
159
|
+
if (normalized === "n" || normalized === "no" || normalized === "아니요" || normalized === "ㄴ") return "denied";
|
|
160
|
+
return "undecided";
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Ensures the machine-level consent state is resolved.
|
|
164
|
+
*
|
|
165
|
+
* Algorithm:
|
|
166
|
+
* 1. Read the machine file.
|
|
167
|
+
* 2. If already decided (granted/denied) → return as-is (no re-prompt).
|
|
168
|
+
* Exception: granted + stale policy_version → revert to undecided (mirrors
|
|
169
|
+
* browser-side resolveEffectiveConsent).
|
|
170
|
+
* 3. If undecided or absent → TTY-prompt once. Persist the answer (including
|
|
171
|
+
* 'undecided' — the directory is guaranteed to exist for subsequent writes,
|
|
172
|
+
* and decided_at is set for potential future reprompt-window logic).
|
|
173
|
+
*
|
|
174
|
+
* Returns the resolved `MachineTelemetryState`. Callers decide what to do with it.
|
|
175
|
+
*
|
|
176
|
+
* NODE-ONLY.
|
|
177
|
+
*/
|
|
178
|
+
async function ensureMachineConsent(policyVersion, deps) {
|
|
179
|
+
const existing = await readMachineState(deps);
|
|
180
|
+
if (existing?.consent === "granted" || existing?.consent === "denied") {
|
|
181
|
+
if (existing.consent === "granted" && existing.policy_version !== policyVersion) {
|
|
182
|
+
await writeMachineState({
|
|
183
|
+
consent: "undecided",
|
|
184
|
+
policy_version: policyVersion
|
|
185
|
+
}, deps);
|
|
186
|
+
return ensureMachineConsent(policyVersion, deps);
|
|
187
|
+
}
|
|
188
|
+
return existing;
|
|
189
|
+
}
|
|
190
|
+
await writeMachineState({
|
|
191
|
+
consent: await (deps?.promptConsent ?? promptTtyConsent)(),
|
|
192
|
+
policy_version: policyVersion
|
|
193
|
+
}, deps);
|
|
194
|
+
return await readMachineState(deps);
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
Object.defineProperty(exports, "ensureMachineConsent", {
|
|
198
|
+
enumerable: true,
|
|
199
|
+
get: function() {
|
|
200
|
+
return ensureMachineConsent;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
Object.defineProperty(exports, "machine_state_exports", {
|
|
204
|
+
enumerable: true,
|
|
205
|
+
get: function() {
|
|
206
|
+
return machine_state_exports;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
Object.defineProperty(exports, "writeMachineState", {
|
|
210
|
+
enumerable: true,
|
|
211
|
+
get: function() {
|
|
212
|
+
return writeMachineState;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
//# sourceMappingURL=machine-state-DOUweFsJ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-state-DOUweFsJ.cjs","names":[],"sources":["../src/telemetry/machine-state.ts"],"sourcesContent":["/**\n * Machine-level telemetry consent store — #542\n *\n * Persists consent to `~/.ait-devtools/telemetry.json` (Node.js side only).\n * This eliminates repeated consent prompts when the dev origin rotates\n * (quick-tunnel host changes per session, localhost port varies).\n *\n * Design invariants:\n * - localStorage keys in src/telemetry/state.ts are LOCKED — this module does\n * NOT replace them. It adds a machine-level overlay that takes precedence\n * when a dev server is running.\n * - Tier 0 / Tier 1 consent semantics are unchanged — only the storage\n * location and lifetime change.\n * - No PII. The file stores: consent enum + decided_at ISO timestamp +\n * policy_version + anon_id (pseudonymous UUID).\n * - anon_id is promoted to machine level so the same identity is used across\n * all dev origins (prevents counting the same developer multiple times).\n *\n * Node.js-only: imported exclusively from the unplugin (server-side).\n * Never imported from panel/browser code.\n */\n\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport type { ConsentState } from './state.js';\n\n// ---------------------------------------------------------------------------\n// Schema\n// ---------------------------------------------------------------------------\n\nexport interface MachineTelemetryState {\n consent: ConsentState;\n /** ISO 8601 timestamp of the decision (or first-write). */\n decided_at: string;\n /** Policy version string at decision time. */\n policy_version: string;\n /**\n * Machine-level anon_id (UUID v4). Created once and never overwritten.\n * Promoted from per-origin localStorage to here so all dev origins share\n * a single pseudonymous identity.\n */\n anon_id: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// File path helpers\n// ---------------------------------------------------------------------------\n\n/** Resolved directory: `~/.ait-devtools`. */\nexport function machineStateDir(homeOverride?: string): string {\n return join(homeOverride ?? homedir(), '.ait-devtools');\n}\n\n/** Resolved file: `~/.ait-devtools/telemetry.json`. */\nexport function machineStateFile(homeOverride?: string): string {\n return join(machineStateDir(homeOverride), 'telemetry.json');\n}\n\n// ---------------------------------------------------------------------------\n// Dependency injection surface (injectable for tests)\n// ---------------------------------------------------------------------------\n\nexport interface MachineStateFsDep {\n existsSync(path: string): boolean;\n mkdirSync(path: string, options?: { recursive?: boolean }): void;\n readFileSync(path: string, encoding: BufferEncoding): string;\n writeFileSync(path: string, data: string, options?: { mode?: number }): void;\n}\n\nexport interface MachineStateDeps {\n fs?: MachineStateFsDep;\n homeDir?: string;\n now?: () => string;\n randomUUID?: () => string;\n /**\n * Override for the TTY consent prompt. When provided, `ensureMachineConsent`\n * calls this instead of the real `promptTtyConsent` — useful in tests where\n * `process.stdin.isTTY` cannot be spied on.\n */\n promptConsent?: () => Promise<ConsentState>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction isoNow(): string {\n return new Date().toISOString();\n}\n\nfunction newUUID(): string {\n return crypto.randomUUID();\n}\n\n/**\n * Returns a minimal MachineStateFsDep using real `node:fs` synchronous APIs.\n * Resolved lazily to avoid pulling node:fs into non-Node environments.\n */\nasync function realFs(): Promise<MachineStateFsDep> {\n const { existsSync, mkdirSync, readFileSync, writeFileSync } = await import('node:fs');\n return { existsSync, mkdirSync, readFileSync, writeFileSync };\n}\n\n// ---------------------------------------------------------------------------\n// Read\n// ---------------------------------------------------------------------------\n\n/**\n * Reads the machine-level telemetry state.\n * Returns `null` when the file is absent or cannot be parsed.\n *\n * NODE-ONLY. Never call from browser/panel code.\n */\nexport async function readMachineState(\n deps?: MachineStateDeps,\n): Promise<MachineTelemetryState | null> {\n const fs = deps?.fs ?? (await realFs());\n const filePath = machineStateFile(deps?.homeDir);\n\n if (!fs.existsSync(filePath)) return null;\n\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, 'utf8');\n } catch {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (\n parsed !== null &&\n typeof parsed === 'object' &&\n 'consent' in parsed &&\n typeof (parsed as { consent: unknown }).consent === 'string' &&\n ['granted', 'denied', 'undecided'].includes((parsed as { consent: string }).consent)\n ) {\n return parsed as MachineTelemetryState;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Write\n// ---------------------------------------------------------------------------\n\n/**\n * Writes (or patches) the machine-level telemetry state file.\n * Creates `~/.ait-devtools/` if it does not yet exist.\n *\n * NODE-ONLY. Never call from browser/panel code.\n */\nexport async function writeMachineState(\n patch: Partial<MachineTelemetryState>,\n deps?: MachineStateDeps,\n): Promise<void> {\n const fs = deps?.fs ?? (await realFs());\n const dir = machineStateDir(deps?.homeDir);\n const filePath = machineStateFile(deps?.homeDir);\n const now = deps?.now ?? isoNow;\n const uuid = deps?.randomUUID ?? newUUID;\n\n // Ensure directory exists.\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Merge with existing state (if any).\n let existing: MachineTelemetryState | null = null;\n if (fs.existsSync(filePath)) {\n try {\n const raw = fs.readFileSync(filePath, 'utf8');\n existing = JSON.parse(raw) as MachineTelemetryState;\n } catch {\n // Corrupt file — overwrite.\n }\n }\n\n const base: MachineTelemetryState = existing ?? {\n consent: 'undecided',\n decided_at: now(),\n policy_version: '',\n anon_id: null,\n };\n\n const next: MachineTelemetryState = {\n ...base,\n ...patch,\n // Never overwrite an existing anon_id — generate one if creating fresh.\n anon_id: existing?.anon_id ?? patch.anon_id ?? uuid(),\n decided_at: now(),\n };\n\n fs.writeFileSync(filePath, JSON.stringify(next, null, 2), { mode: 0o600 });\n}\n\n// ---------------------------------------------------------------------------\n// TTY consent prompt (dev server only)\n// ---------------------------------------------------------------------------\n\n/**\n * Prompts the user in the terminal (stdin/stdout TTY) to accept or deny\n * telemetry consent — called once per machine on the first `pnpm dev` run.\n *\n * Returns the chosen `ConsentState`, or `'undecided'` when:\n * - The TTY is not interactive (CI, headless, piped stdin).\n * - The prompt times out.\n * - An error occurs.\n *\n * NODE-ONLY. Called from the unplugin `configureServer` hook.\n */\nexport async function promptTtyConsent(): Promise<ConsentState> {\n // Guard: require an interactive terminal on both ends.\n if (!process.stdin.isTTY || !process.stdout.isTTY) {\n return 'undecided';\n }\n\n const { createInterface } = await import('node:readline');\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n\n const question = (q: string): Promise<string> =>\n new Promise((resolve) => rl.question(q, resolve));\n\n process.stdout.write(\n '\\n[@ait-co/devtools] 익명 사용 통계를 보내도 될까요?\\n' +\n ' 버전·날짜만 수집하고 PII는 없습니다. 언제든 Environment 탭에서 끌 수 있어요.\\n' +\n ' 자세히: https://docs.aitc.dev/privacy\\n' +\n ' (y) 네, 보낼게요 (n) 아니요 (무응답 시 보류 — 다음 기동 때 다시 물어요)\\n',\n );\n\n let answer = '';\n try {\n // 30-second timeout — if the user ignores the prompt, default to 'undecided'.\n answer = await Promise.race([\n question(' > '),\n new Promise<string>((resolve) => setTimeout(() => resolve(''), 30_000)),\n ]);\n } catch {\n // readline error — stay undecided.\n } finally {\n rl.close();\n }\n\n const normalized = answer.trim().toLowerCase();\n if (normalized === 'y' || normalized === 'yes' || normalized === '네' || normalized === 'ㅇ') {\n return 'granted';\n }\n if (normalized === 'n' || normalized === 'no' || normalized === '아니요' || normalized === 'ㄴ') {\n return 'denied';\n }\n // Empty / timeout / unrecognised → undecided (will be asked again next session).\n return 'undecided';\n}\n\n// ---------------------------------------------------------------------------\n// Bootstrap helper — called from configureServer once dev server is ready\n// ---------------------------------------------------------------------------\n\n/**\n * Ensures the machine-level consent state is resolved.\n *\n * Algorithm:\n * 1. Read the machine file.\n * 2. If already decided (granted/denied) → return as-is (no re-prompt).\n * Exception: granted + stale policy_version → revert to undecided (mirrors\n * browser-side resolveEffectiveConsent).\n * 3. If undecided or absent → TTY-prompt once. Persist the answer (including\n * 'undecided' — the directory is guaranteed to exist for subsequent writes,\n * and decided_at is set for potential future reprompt-window logic).\n *\n * Returns the resolved `MachineTelemetryState`. Callers decide what to do with it.\n *\n * NODE-ONLY.\n */\nexport async function ensureMachineConsent(\n policyVersion: string,\n deps?: MachineStateDeps,\n): Promise<MachineTelemetryState> {\n const existing = await readMachineState(deps);\n\n if (existing?.consent === 'granted' || existing?.consent === 'denied') {\n // Policy-version bump: re-prompt only when previously granted (same logic as\n // browser-side resolveEffectiveConsent — denied stays denied on version change).\n if (existing.consent === 'granted' && existing.policy_version !== policyVersion) {\n await writeMachineState({ consent: 'undecided', policy_version: policyVersion }, deps);\n // Recurse: will now prompt the user.\n return ensureMachineConsent(policyVersion, deps);\n }\n return existing;\n }\n\n // Undecided or absent → prompt (use injected override in tests).\n const prompt = deps?.promptConsent ?? promptTtyConsent;\n const answer = await prompt();\n await writeMachineState({ consent: answer, policy_version: policyVersion }, deps);\n\n // Re-read to get the full written state (includes newly generated anon_id).\n return (await readMachineState(deps)) as MachineTelemetryState;\n}\n"],"mappings":"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"}
|