@itpay/cli 0.1.10 → 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/lib/env.js ADDED
@@ -0,0 +1,713 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { execFileSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ export const DEFAULT_API_BASE = process.env.ITPAY_API_BASE || process.env.ITPAY_CORE_API_BASE || process.env.ITPAY_CORE_BASE_URL || "https://dev.api.itpay.ai";
9
+ export const CONFIG_DIR = path.join(os.homedir(), ".itp");
10
+ export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
11
+ export const STATE_PATH = path.join(CONFIG_DIR, "state.json");
12
+ export const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
13
+ export const RUNS_DIR = path.join(CONFIG_DIR, "runs");
14
+ export const LOCK_PATH = path.join(CONFIG_DIR, "state.lock");
15
+ const PACKAGE_ROOT_FROM_ENV = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
16
+ export const CLI_FILE = fs.existsSync(path.join(PACKAGE_ROOT_FROM_ENV, "bin", "itp"))
17
+ ? path.join(PACKAGE_ROOT_FROM_ENV, "bin", "itp")
18
+ : path.join(PACKAGE_ROOT_FROM_ENV, "bin", "itp.js");
19
+ export const CLI_DIR = path.dirname(CLI_FILE);
20
+ export const PACKAGE_ROOT = PACKAGE_ROOT_FROM_ENV;
21
+ export const VERSION = packageVersion();
22
+
23
+ function csvValues(value) {
24
+ if (value === undefined || value === null || value === false) return [];
25
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
26
+ return String(value).split(",").map((item) => item.trim()).filter(Boolean);
27
+ }
28
+
29
+ function booleanFlag(value) {
30
+ if (value === true || value === false) return value;
31
+ const normalized = String(value).trim().toLowerCase();
32
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
33
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
34
+ throw new Error(`invalid boolean flag value: ${value}`);
35
+ }
36
+
37
+ function intFlag(value, name) {
38
+ const number = Number(value);
39
+ if (!Number.isInteger(number)) throw new Error(`${name} must be an integer`);
40
+ return number;
41
+ }
42
+
43
+ function splitCSV(value) {
44
+ return String(value || "")
45
+ .split(",")
46
+ .map((part) => part.trim())
47
+ .filter(Boolean);
48
+ }
49
+
50
+ function queryString(params) {
51
+ const raw = params.toString();
52
+ return raw ? `?${raw}` : "";
53
+ }
54
+
55
+ function appendURLQuery(target, params) {
56
+ const suffix = params.toString();
57
+ if (!suffix) return target;
58
+ return `${target}${String(target).includes("?") ? "&" : "?"}${suffix}`;
59
+ }
60
+
61
+ function positional(values, index) {
62
+ const value = values[index];
63
+ if (!value || String(value).startsWith("--")) return "";
64
+ return String(value);
65
+ }
66
+
67
+ function positionalArgs(values = []) {
68
+ const result = [];
69
+ for (let i = 0; i < values.length; i += 1) {
70
+ const value = values[i];
71
+ if (!value) continue;
72
+ if (String(value).startsWith("--")) {
73
+ const next = values[i + 1];
74
+ if (next && !String(next).startsWith("--")) i += 1;
75
+ continue;
76
+ }
77
+ result.push(String(value));
78
+ }
79
+ return result;
80
+ }
81
+
82
+ function stripInternalBuyerFields(value) {
83
+ if (Array.isArray(value)) return value.map(stripInternalBuyerFields);
84
+ if (!value || typeof value !== "object") return value;
85
+ const result = {};
86
+ for (const [key, nested] of Object.entries(value)) {
87
+ if (key === "next_actions") continue;
88
+ result[key] = stripInternalBuyerFields(nested);
89
+ }
90
+ return result;
91
+ }
92
+
93
+ function apiTimeoutMs(flags = {}) {
94
+ const seconds = Number(flags.api_timeout || process.env.ITP_API_TIMEOUT_SECONDS || 45);
95
+ if (!Number.isFinite(seconds) || seconds <= 0) return 45000;
96
+ return Math.max(5000, seconds * 1000);
97
+ }
98
+
99
+ function parseFlags(args) {
100
+ const flags = {};
101
+ for (let i = 0; i < args.length; i += 1) {
102
+ const arg = args[i];
103
+ if (!arg.startsWith("--")) continue;
104
+ const key = arg.slice(2).replaceAll("-", "_");
105
+ const next = args[i + 1];
106
+ if (!next || next.startsWith("--")) {
107
+ flags[key] = true;
108
+ } else {
109
+ flags[key] = next;
110
+ i += 1;
111
+ }
112
+ }
113
+ return flags;
114
+ }
115
+
116
+ function normalizePurchaseFlags(flags, required = false) {
117
+ const plan = typeof flags.plan === "string" ? flags.plan.trim() : "";
118
+ const rawCredits = flags.credits ?? flags.credit ?? null;
119
+ const hasCredits = rawCredits !== null && rawCredits !== undefined && rawCredits !== false;
120
+ if (plan && hasCredits) {
121
+ throw new Error("use either --plan or --credits, not both");
122
+ }
123
+ if (hasCredits) {
124
+ const credits = Number(rawCredits);
125
+ if (!Number.isInteger(credits) || credits < 20) {
126
+ throw new Error("--credits must be an integer greater than or equal to 20");
127
+ }
128
+ return {
129
+ kind: "custom",
130
+ plan: null,
131
+ credits,
132
+ key: `credits-${credits}`
133
+ };
134
+ }
135
+ if (plan) {
136
+ if (plan === "coding-100") {
137
+ throw new Error("coding-100 is disabled; use credit-100, credit-300, credit-500, or --credits <amount>");
138
+ }
139
+ return {
140
+ kind: "plan",
141
+ plan,
142
+ credits: null,
143
+ key: `plan-${plan}`
144
+ };
145
+ }
146
+ if (required) {
147
+ throw new Error("choose a purchase: --credits <integer >=20> or --plan credit-100|credit-300|credit-500");
148
+ }
149
+ return { kind: null, plan: null, credits: null, key: "none" };
150
+ }
151
+
152
+ function apiBase(flags = {}, config = readConfig()) {
153
+ return (flags.api_base || config.api_base || DEFAULT_API_BASE).replace(/\/$/, "");
154
+ }
155
+
156
+ function readConfig() {
157
+ return readJSON(CONFIG_PATH, {});
158
+ }
159
+
160
+ function packageVersion() {
161
+ for (const file of [
162
+ path.join(PACKAGE_ROOT, "package.json"),
163
+ path.join(PACKAGE_ROOT, "share", "itpay_cli", "package.json")
164
+ ]) {
165
+ try {
166
+ return JSON.parse(fs.readFileSync(file, "utf8")).version || "0.0.0";
167
+ } catch {
168
+ // Try the next install layout.
169
+ }
170
+ }
171
+ return "0.0.0";
172
+ }
173
+
174
+ function writeConfig(value) {
175
+ writeJSON0600(CONFIG_PATH, value);
176
+ }
177
+
178
+ function readState() {
179
+ return readJSON(STATE_PATH, {});
180
+ }
181
+
182
+ function writeState(value) {
183
+ writeJSON0600(STATE_PATH, value);
184
+ }
185
+
186
+ function runPath(runId) {
187
+ return path.join(RUNS_DIR, `${runId}.json`);
188
+ }
189
+
190
+ function readRun(runId) {
191
+ if (!runId) return null;
192
+ return readJSON(runPath(runId), null);
193
+ }
194
+
195
+ function listRuns() {
196
+ if (!fs.existsSync(RUNS_DIR)) return [];
197
+ return fs.readdirSync(RUNS_DIR)
198
+ .filter((name) => name.endsWith(".json"))
199
+ .map((name) => readJSON(path.join(RUNS_DIR, name), null))
200
+ .filter(Boolean)
201
+ .sort((a, b) => String(b.updated_at || "").localeCompare(String(a.updated_at || "")));
202
+ }
203
+
204
+ function writeRun(run) {
205
+ if (!run?.run_id) throw new Error("run_id is required");
206
+ ensureConfigDir();
207
+ fs.mkdirSync(RUNS_DIR, { recursive: true });
208
+ try {
209
+ fs.chmodSync(RUNS_DIR, 0o700);
210
+ } catch {
211
+ // Best effort on platforms without POSIX modes.
212
+ }
213
+ const next = {
214
+ ...run,
215
+ schema_version: "itp.run.v1",
216
+ updated_at: new Date().toISOString()
217
+ };
218
+ const file = runPath(next.run_id);
219
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
220
+ fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
221
+ fs.renameSync(tmp, file);
222
+ try {
223
+ fs.chmodSync(file, 0o600);
224
+ } catch {
225
+ // Best effort on platforms without POSIX modes.
226
+ }
227
+ writeState({ ...readState(), current_run_id: next.run_id });
228
+ return next;
229
+ }
230
+
231
+ function mergeRun(run, patch) {
232
+ return {
233
+ ...(run || {}),
234
+ ...patch,
235
+ auth: patch.auth === undefined ? run?.auth : { ...(run?.auth || {}), ...(patch.auth || {}) },
236
+ account: patch.account === undefined ? run?.account : { ...(run?.account || {}), ...(patch.account || {}) },
237
+ checkout: patch.checkout === undefined ? run?.checkout : { ...(run?.checkout || {}), ...(patch.checkout || {}) },
238
+ payment: patch.payment === undefined ? run?.payment : { ...(run?.payment || {}), ...(patch.payment || {}) },
239
+ grant: patch.grant === undefined ? run?.grant : { ...(run?.grant || {}), ...(patch.grant || {}) },
240
+ result: patch.result === undefined ? run?.result : { ...(run?.result || {}), ...(patch.result || {}) }
241
+ };
242
+ }
243
+
244
+ function updateRun(run, patch) {
245
+ return writeRun(mergeRun(run, patch));
246
+ }
247
+
248
+ function updateCurrentRun(patch, flags = {}) {
249
+ const run = readRun(flags.run_id || readState().current_run_id);
250
+ if (!run) return null;
251
+ return writeRun(mergeRun(run, patch));
252
+ }
253
+
254
+ function prepareSetupRun(flags, options) {
255
+ const explicitRunId = flags.run_id || null;
256
+ const state = readState();
257
+ let run = explicitRunId ? readRun(explicitRunId) : (!flags.new_run ? readRun(state.current_run_id) : null);
258
+ const reusable = run
259
+ && !["done", "installed", "failed", "cancelled"].includes(run.status)
260
+ && run.phase !== "done"
261
+ && (!run.plan_id || run.plan_id === options.plan)
262
+ && (options.plan || !run.credits || Number(run.credits) === Number(options.credits || 0))
263
+ && (!run.payment_method || run.payment_method === options.method);
264
+ if (reusable) {
265
+ return writeRun(mergeRun(run, {
266
+ target: options.target,
267
+ plan_id: options.plan,
268
+ credits: options.credits,
269
+ purchase_kind: options.plan ? "plan" : "custom",
270
+ payment_method: options.method,
271
+ agent_host: flags.host || run.agent_host || null,
272
+ agent_display: flags.display || run.agent_display || null,
273
+ agent_qr_format: flags.qr_format || run.agent_qr_format || null,
274
+ install_runtime: Boolean(options.install_runtime),
275
+ status: "running"
276
+ }));
277
+ }
278
+ if (flags.resume && explicitRunId && !run) {
279
+ throw new Error(`run not found: ${explicitRunId}`);
280
+ }
281
+ const runId = explicitRunId || `run_${cryptoRandom()}`;
282
+ return writeRun({
283
+ schema_version: "itp.run.v1",
284
+ run_id: runId,
285
+ created_at: new Date().toISOString(),
286
+ api_base: apiBase(flags),
287
+ target: options.target,
288
+ install_runtime: Boolean(options.install_runtime),
289
+ plan_id: options.plan,
290
+ credits: options.credits,
291
+ purchase_kind: options.plan ? "plan" : "custom",
292
+ payment_method: options.method,
293
+ agent_host: flags.host || null,
294
+ agent_display: flags.display || null,
295
+ agent_qr_format: flags.qr_format || null,
296
+ idempotency_key: flags.idempotency_key || `setup:${runId}:${options.plan || `credits-${options.credits}`}`,
297
+ phase: "new",
298
+ status: "running",
299
+ safe_summary: "Setup started."
300
+ });
301
+ }
302
+
303
+ async function withStateLock(fn) {
304
+ ensureConfigDir();
305
+ const staleMs = 10 * 60 * 1000;
306
+ try {
307
+ const stat = fs.statSync(LOCK_PATH);
308
+ const lock = readJSON(LOCK_PATH, {});
309
+ if ((lock.pid && !processIsRunning(lock.pid)) || Date.now() - stat.mtimeMs > staleMs) {
310
+ fs.unlinkSync(LOCK_PATH);
311
+ }
312
+ } catch {
313
+ // No lock or unreadable stale state.
314
+ }
315
+ let fd;
316
+ try {
317
+ fd = fs.openSync(LOCK_PATH, "wx", 0o600);
318
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
319
+ } catch {
320
+ const error = new Error(`another itp setup/status operation is running; if no other ItPay CLI is active, remove the stale lock and retry: rm -f ${LOCK_PATH}`);
321
+ error.next = [
322
+ { type: "check_status", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true },
323
+ { type: "clear_stale_lock", command: `rm -f ${shellQuote(LOCK_PATH)}`, safe_for_agent: true }
324
+ ];
325
+ throw error;
326
+ } finally {
327
+ if (fd !== undefined) fs.closeSync(fd);
328
+ }
329
+ try {
330
+ return await fn();
331
+ } finally {
332
+ try {
333
+ fs.unlinkSync(LOCK_PATH);
334
+ } catch {
335
+ // Best effort cleanup.
336
+ }
337
+ }
338
+ }
339
+
340
+ function readCredentials() {
341
+ return readJSON(CREDENTIALS_PATH, {});
342
+ }
343
+
344
+ function writeCredentials(value) {
345
+ writeJSON0600(CREDENTIALS_PATH, value);
346
+ }
347
+
348
+ function writeJSON0600(file, value) {
349
+ ensureConfigDir();
350
+ fs.writeFileSync(file, JSON.stringify(value, null, 2), { mode: 0o600 });
351
+ fs.chmodSync(file, 0o600);
352
+ }
353
+
354
+ function writeSessionCredentials(response) {
355
+ const currentAccountId = readConfig().account_id;
356
+ let credentials = deleteSessionCredential(readCredentials());
357
+ if (currentAccountId && currentAccountId !== response.account_id) {
358
+ for (const key of Object.keys(credentials)) {
359
+ if (key.startsWith("grant_")) {
360
+ const grantId = key.slice("grant_".length);
361
+ deleteGrantCredential(grantId);
362
+ }
363
+ }
364
+ credentials = {};
365
+ }
366
+ writeCredentials({ ...credentials, ...storeSessionCredential(response) });
367
+ }
368
+
369
+ function storeSessionCredential(response) {
370
+ const token = response.session_token;
371
+ const ref = `itpay:session:${response.account_id}:${response.device_id}`;
372
+ const nativeStore = writeNativeSecret(ref, token);
373
+ if (nativeStore.ok) {
374
+ return {
375
+ session_token_store: nativeStore.store,
376
+ session_token_ref: nativeStore.ref
377
+ };
378
+ }
379
+ return {
380
+ session_token: token,
381
+ session_token_store: "file",
382
+ session_token_warning: nativeStore.error
383
+ };
384
+ }
385
+
386
+ function readSessionToken(credentials = readCredentials()) {
387
+ if (credentials.session_token) {
388
+ return credentials.session_token;
389
+ }
390
+ if (credentials.session_token_store && credentials.session_token_ref) {
391
+ return readNativeSecret(credentials.session_token_store, credentials.session_token_ref);
392
+ }
393
+ return "";
394
+ }
395
+
396
+ function deleteSessionCredential(credentials) {
397
+ if (!credentials) {
398
+ return {};
399
+ }
400
+ if (credentials.session_token_store && credentials.session_token_ref) {
401
+ deleteNativeSecret(credentials.session_token_store, credentials.session_token_ref);
402
+ }
403
+ delete credentials.session_token;
404
+ delete credentials.session_token_store;
405
+ delete credentials.session_token_ref;
406
+ delete credentials.session_token_warning;
407
+ return credentials;
408
+ }
409
+
410
+ function sanitizeAuthResponse(response) {
411
+ const { session_token, ...safe } = response;
412
+ return { ...safe, session_stored: Boolean(session_token) };
413
+ }
414
+
415
+ function storeGrantCredential(grantId, credential) {
416
+ const credentials = readCredentials();
417
+ const key = credential.key;
418
+ const record = { ...credential };
419
+ delete record.key;
420
+ const nativeStore = writeNativeSecret(grantSecretRef(grantId), key);
421
+ if (nativeStore.ok) {
422
+ record.credential_store = nativeStore.store;
423
+ record.credential_ref = nativeStore.ref;
424
+ } else {
425
+ record.key = key;
426
+ record.credential_store = "file";
427
+ record.credential_warning = nativeStore.error;
428
+ }
429
+ credentials[`grant_${grantId}`] = record;
430
+ writeCredentials(credentials);
431
+ return record;
432
+ }
433
+
434
+ function readGrantCredential(grantId) {
435
+ const record = readCredentials()[`grant_${grantId}`];
436
+ if (!record) return null;
437
+ if (record.key) return record;
438
+ const key = readNativeSecret(record.credential_store, record.credential_ref);
439
+ return key ? { ...record, key } : record;
440
+ }
441
+
442
+ function deleteGrantCredential(grantId) {
443
+ const credentials = readCredentials();
444
+ const record = credentials[`grant_${grantId}`];
445
+ if (record?.credential_store && record?.credential_ref) {
446
+ deleteNativeSecret(record.credential_store, record.credential_ref);
447
+ }
448
+ delete credentials[`grant_${grantId}`];
449
+ writeCredentials(credentials);
450
+ }
451
+
452
+ function grantSecretRef(grantId) {
453
+ return `itpay:${grantId}`;
454
+ }
455
+
456
+ function detectNativeCredentialStore() {
457
+ if (!shouldUseNativeCredentialStore()) return "file";
458
+ if (process.platform === "darwin" && commandExists("security")) return "macos-keychain";
459
+ if (process.platform === "linux" && commandExists("secret-tool")) return "secret-tool";
460
+ return "unavailable";
461
+ }
462
+
463
+ function writeNativeSecret(ref, secret) {
464
+ if (!secret) return { ok: false, error: "empty secret" };
465
+ if (!shouldUseNativeCredentialStore()) {
466
+ return { ok: false, error: "native credential store disabled for non-interactive agent host" };
467
+ }
468
+ if (process.platform === "darwin" && commandExists("security")) {
469
+ try {
470
+ execFileSync("security", [
471
+ "add-generic-password",
472
+ "-a",
473
+ ref,
474
+ "-s",
475
+ "ItPay",
476
+ "-w",
477
+ secret,
478
+ "-U"
479
+ ], { stdio: "ignore" });
480
+ return { ok: true, store: "macos-keychain", ref };
481
+ } catch (error) {
482
+ return { ok: false, error: `macOS Keychain unavailable: ${error.message}` };
483
+ }
484
+ }
485
+ if (process.platform === "linux" && commandExists("secret-tool")) {
486
+ try {
487
+ execFileSync("secret-tool", [
488
+ "store",
489
+ "--label=ItPay",
490
+ "service",
491
+ "ItPay",
492
+ "account",
493
+ ref
494
+ ], { input: secret, stdio: ["pipe", "ignore", "ignore"] });
495
+ return { ok: true, store: "secret-tool", ref };
496
+ } catch (error) {
497
+ return { ok: false, error: `secret-tool unavailable: ${error.message}` };
498
+ }
499
+ }
500
+ return { ok: false, error: "native credential store unavailable" };
501
+ }
502
+
503
+ function shouldUseNativeCredentialStore() {
504
+ const store = String(process.env.ITP_CREDENTIAL_STORE || "").toLowerCase();
505
+ if (store === "file") return false;
506
+ if (store === "native" || store === "keychain" || store === "secret-tool") return true;
507
+ const disabled = String(process.env.ITP_DISABLE_NATIVE_CREDENTIAL_STORE || "").toLowerCase();
508
+ if (["1", "true", "yes"].includes(disabled)) return false;
509
+ if (process.env.CODEX_CI || process.env.CODEX_SHELL || process.env.CODEX_THREAD_ID) return false;
510
+ if (process.env.CI && !process.env.GITHUB_ACTIONS) return false;
511
+ return true;
512
+ }
513
+
514
+ function readNativeSecret(store, ref) {
515
+ if (!store || !ref) return "";
516
+ try {
517
+ if (store === "macos-keychain") {
518
+ return execFileSync("security", [
519
+ "find-generic-password",
520
+ "-a",
521
+ ref,
522
+ "-s",
523
+ "ItPay",
524
+ "-w"
525
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
526
+ }
527
+ if (store === "secret-tool") {
528
+ return execFileSync("secret-tool", [
529
+ "lookup",
530
+ "service",
531
+ "ItPay",
532
+ "account",
533
+ ref
534
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
535
+ }
536
+ } catch {
537
+ return "";
538
+ }
539
+ return "";
540
+ }
541
+
542
+ function deleteNativeSecret(store, ref) {
543
+ try {
544
+ if (store === "macos-keychain") {
545
+ execFileSync("security", [
546
+ "delete-generic-password",
547
+ "-a",
548
+ ref,
549
+ "-s",
550
+ "ItPay"
551
+ ], { stdio: "ignore" });
552
+ }
553
+ if (store === "secret-tool") {
554
+ execFileSync("secret-tool", [
555
+ "clear",
556
+ "service",
557
+ "ItPay",
558
+ "account",
559
+ ref
560
+ ], { stdio: "ignore" });
561
+ }
562
+ } catch {
563
+ // The local record is still removed; missing native secrets are harmless.
564
+ }
565
+ }
566
+
567
+ function commandExists(command) {
568
+ try {
569
+ execFileSync("which", [command], { stdio: "ignore" });
570
+ return true;
571
+ } catch {
572
+ return false;
573
+ }
574
+ }
575
+
576
+ function processIsRunning(pid) {
577
+ const numericPid = Number(pid);
578
+ if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
579
+ try {
580
+ process.kill(numericPid, 0);
581
+ return true;
582
+ } catch {
583
+ return false;
584
+ }
585
+ }
586
+
587
+ function ensureConfigDir() {
588
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
589
+ try {
590
+ fs.chmodSync(CONFIG_DIR, 0o700);
591
+ } catch {
592
+ // Best effort; individual secret files are still written as 0600.
593
+ }
594
+ }
595
+
596
+ function readText(file, fallback) {
597
+ try {
598
+ return fs.readFileSync(file, "utf8");
599
+ } catch {
600
+ return fallback;
601
+ }
602
+ }
603
+
604
+ function readJSON(file, fallback) {
605
+ try {
606
+ return JSON.parse(fs.readFileSync(file, "utf8"));
607
+ } catch {
608
+ return fallback;
609
+ }
610
+ }
611
+
612
+ function writeJSONWithBackup(file, value, dryRun) {
613
+ return writeTextWithBackup(file, `${JSON.stringify(value, null, 2)}\n`, 0o600, dryRun);
614
+ }
615
+
616
+ function writeTextWithBackup(file, content, mode, dryRun) {
617
+ const backupPath = fs.existsSync(file) ? `${file}.itp-bak-${Date.now()}` : "";
618
+ if (dryRun) {
619
+ return { action: fs.existsSync(file) ? "would_update" : "would_create", backup_path: backupPath || null };
620
+ }
621
+ const dir = path.dirname(file);
622
+ if (dir === CONFIG_DIR) {
623
+ ensureConfigDir();
624
+ } else {
625
+ fs.mkdirSync(dir, { recursive: true });
626
+ }
627
+ if (backupPath) {
628
+ fs.copyFileSync(file, backupPath);
629
+ fs.chmodSync(backupPath, mode);
630
+ }
631
+ fs.writeFileSync(file, content, { mode });
632
+ fs.chmodSync(file, mode);
633
+ return { action: backupPath ? "updated" : "created", backup_path: backupPath || null };
634
+ }
635
+
636
+ function fileMode(file) {
637
+ try {
638
+ return `0${(fs.statSync(file).mode & 0o777).toString(8)}`;
639
+ } catch {
640
+ return null;
641
+ }
642
+ }
643
+
644
+ function replaceManagedBlock(source, name, block) {
645
+ const start = `# >>> itp ${name}`;
646
+ const end = `# <<< itp ${name}`;
647
+ const managed = `${start}\n${block.trim()}\n${end}`;
648
+ const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`, "m");
649
+ const trimmed = source.trimEnd();
650
+ if (pattern.test(source)) return source.replace(pattern, managed);
651
+ return `${trimmed}${trimmed ? "\n\n" : ""}${managed}\n`;
652
+ }
653
+
654
+ function escapeRegExp(value) {
655
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
656
+ }
657
+
658
+ function escapeTomlString(value) {
659
+ return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
660
+ }
661
+
662
+ function currentExecutable() {
663
+ return process.argv[1] || "itp";
664
+ }
665
+
666
+ function quoteShell(value) {
667
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
668
+ }
669
+
670
+ function output(value) {
671
+ console.log(JSON.stringify(value, null, 2));
672
+ }
673
+
674
+ function outputError(error) {
675
+ const payload = { success: false, message: safeErrorMessage(error) };
676
+ if (error?.next) payload.next = error.next;
677
+ console.error(JSON.stringify(payload, null, 2));
678
+ }
679
+
680
+ function maskSecret(secret) {
681
+ if (!secret) return "";
682
+ if (secret.length <= 8) return "********";
683
+ return `${secret.slice(0, 4)}********${secret.slice(-4)}`;
684
+ }
685
+
686
+ function cryptoRandom() {
687
+ return crypto.randomUUID();
688
+ }
689
+
690
+ function sleep(ms) {
691
+ return new Promise((resolve) => setTimeout(resolve, ms));
692
+ }
693
+
694
+ function cliCommand(...args) {
695
+ const override = process.env.ITP_COMMAND;
696
+ const base = override
697
+ ? override
698
+ : `${shellQuote(process.execPath)} ${shellQuote(CLI_FILE)}`;
699
+ return [base, ...args.map((arg) => shellQuote(String(arg)))].join(" ");
700
+ }
701
+
702
+ function shellQuote(value) {
703
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
704
+ return `'${value.replace(/'/g, "'\\''")}'`;
705
+ }
706
+
707
+ function safeErrorMessage(error) {
708
+ return String(error?.message || error || "unknown error")
709
+ .replace(/itp_sess_[A-Za-z0-9_-]+/g, "itp_sess_****")
710
+ .replace(/sk-[A-Za-z0-9_-]+/g, "sk-****");
711
+ }
712
+
713
+ export { csvValues, booleanFlag, intFlag, splitCSV, queryString, appendURLQuery, positional, positionalArgs, stripInternalBuyerFields, apiTimeoutMs, parseFlags, normalizePurchaseFlags, apiBase, readConfig, packageVersion, writeConfig, readState, writeState, runPath, readRun, listRuns, writeRun, mergeRun, updateRun, updateCurrentRun, prepareSetupRun, withStateLock, readCredentials, writeCredentials, writeJSON0600, writeSessionCredentials, storeSessionCredential, readSessionToken, deleteSessionCredential, sanitizeAuthResponse, storeGrantCredential, readGrantCredential, deleteGrantCredential, grantSecretRef, detectNativeCredentialStore, writeNativeSecret, shouldUseNativeCredentialStore, readNativeSecret, deleteNativeSecret, commandExists, processIsRunning, ensureConfigDir, readText, readJSON, writeJSONWithBackup, writeTextWithBackup, fileMode, replaceManagedBlock, escapeRegExp, escapeTomlString, currentExecutable, quoteShell, output, outputError, maskSecret, cryptoRandom, sleep, cliCommand, shellQuote, safeErrorMessage };