@athsra/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * .env 형식 parser/serializer (KEY=value, # comment, blank lines).
3
+ * Phase 0 minimal — escape, multiline, $expansion 미지원.
4
+ */
5
+
6
+ export function parseEnv(text: string): Record<string, string> {
7
+ const out: Record<string, string> = {};
8
+ for (const line of text.split('\n')) {
9
+ const trimmed = line.trim();
10
+ if (!trimmed || trimmed.startsWith('#')) continue;
11
+ const eqIdx = trimmed.indexOf('=');
12
+ if (eqIdx < 0) continue;
13
+ const key = trimmed.slice(0, eqIdx).trim();
14
+ let value = trimmed.slice(eqIdx + 1).trim();
15
+ // strip surrounding quotes
16
+ if (
17
+ (value.startsWith('"') && value.endsWith('"')) ||
18
+ (value.startsWith("'") && value.endsWith("'"))
19
+ ) {
20
+ value = value.slice(1, -1);
21
+ }
22
+ out[key] = value;
23
+ }
24
+ return out;
25
+ }
26
+
27
+ export function serializeEnv(plain: Record<string, string>): string {
28
+ return Object.entries(plain)
29
+ .map(([k, v]) => `${k}=${v}`)
30
+ .join('\n');
31
+ }
@@ -0,0 +1,65 @@
1
+ import { Entry } from '@napi-rs/keyring';
2
+
3
+ const SERVICE = 'athsra';
4
+
5
+ function entry(account: string): Entry {
6
+ return new Entry(SERVICE, account);
7
+ }
8
+
9
+ export function setMasterPw(machineId: string, pw: string): void {
10
+ entry(`master-pw:${machineId}`).setPassword(pw);
11
+ }
12
+
13
+ export function getMasterPw(machineId: string): string | null {
14
+ try {
15
+ return entry(`master-pw:${machineId}`).getPassword();
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export function clearMasterPw(machineId: string): void {
22
+ try {
23
+ entry(`master-pw:${machineId}`).deletePassword();
24
+ } catch {
25
+ /* ignore */
26
+ }
27
+ }
28
+
29
+ export function setToken(machineId: string, token: string): void {
30
+ entry(`token:${machineId}`).setPassword(token);
31
+ }
32
+
33
+ export function getToken(machineId: string): string | null {
34
+ try {
35
+ return entry(`token:${machineId}`).getPassword();
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ export function clearToken(machineId: string): void {
42
+ try {
43
+ entry(`token:${machineId}`).deletePassword();
44
+ } catch {
45
+ /* ignore */
46
+ }
47
+ }
48
+
49
+ export interface ProbeResult {
50
+ ok: boolean;
51
+ error?: string;
52
+ }
53
+
54
+ export function probeKeyring(): ProbeResult {
55
+ try {
56
+ const probe = entry('__probe__');
57
+ probe.setPassword('1');
58
+ const got = probe.getPassword();
59
+ probe.deletePassword();
60
+ if (got === '1') return { ok: true };
61
+ return { ok: false, error: 'set/get round-trip mismatch' };
62
+ } catch (err) {
63
+ return { ok: false, error: (err as Error).message };
64
+ }
65
+ }
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync, unlinkSync } from 'node:fs';
2
+ import { SESSION_FILE } from './config.ts';
3
+
4
+ interface LegacySession {
5
+ masterPw: string;
6
+ expiresAt: number;
7
+ }
8
+
9
+ /**
10
+ * Phase 0 plaintext session file (~/.athsra/session, 8h TTL, 0600)
11
+ * 의 1회성 migration helper. file 발견 시 master pw 반환 + file 삭제.
12
+ *
13
+ * Phase 1+: keyring (`@napi-rs/keyring`) 표준. 이 함수는
14
+ * 기존 사용자 1회 migration 후 영구 deprecated.
15
+ */
16
+ export function consumeLegacySession(): { masterPw: string } | null {
17
+ if (!existsSync(SESSION_FILE)) return null;
18
+ let parsed: LegacySession | null = null;
19
+ try {
20
+ const raw = readFileSync(SESSION_FILE, 'utf-8');
21
+ if (raw) {
22
+ const json = JSON.parse(raw) as LegacySession;
23
+ if (typeof json.masterPw === 'string' && json.masterPw.length > 0) {
24
+ parsed = json;
25
+ }
26
+ }
27
+ } catch {
28
+ /* fall through to delete */
29
+ }
30
+ try {
31
+ unlinkSync(SESSION_FILE);
32
+ } catch {
33
+ /* ignore */
34
+ }
35
+ if (!parsed) return null;
36
+ return { masterPw: parsed.masterPw };
37
+ }
@@ -0,0 +1,33 @@
1
+ import prompts from 'prompts';
2
+
3
+ export async function promptPassword(message: string): Promise<string> {
4
+ const response = await prompts({
5
+ type: 'password',
6
+ name: 'value',
7
+ message,
8
+ });
9
+ if (!response.value) {
10
+ throw new Error('cancelled');
11
+ }
12
+ return response.value as string;
13
+ }
14
+
15
+ export async function promptText(message: string, initial?: string): Promise<string> {
16
+ const response = await prompts({
17
+ type: 'text',
18
+ name: 'value',
19
+ message,
20
+ initial,
21
+ });
22
+ return (response.value as string) ?? '';
23
+ }
24
+
25
+ export async function promptConfirm(message: string, initial = false): Promise<boolean> {
26
+ const response = await prompts({
27
+ type: 'confirm',
28
+ name: 'value',
29
+ message,
30
+ initial,
31
+ });
32
+ return Boolean(response.value);
33
+ }