@manan_joshi/atelier 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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/index.js +2342 -0
  3. package/package.json +34 -0
package/dist/index.js ADDED
@@ -0,0 +1,2342 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/index.tsx
5
+ import { execFileSync as execFileSync4 } from "child_process";
6
+ import { stdin as input, stdout as output } from "process";
7
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
8
+ import { homedir as homedir4, platform as platform3 } from "os";
9
+ import { dirname as dirname4, join as join4, resolve as resolve2 } from "path";
10
+ import { Command } from "commander";
11
+
12
+ // ../../packages/auth-client/src/index.ts
13
+ import { execFileSync } from "child_process";
14
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
15
+ import { homedir, platform } from "os";
16
+ import { dirname, join } from "path";
17
+ var CONFIG_DIR = join(homedir(), ".config", "atelier");
18
+ var SESSION_PATH = join(CONFIG_DIR, "session.json");
19
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
20
+ var KEYCHAIN_SERVICE = "dev.atelier.session";
21
+ var DEFAULT_API_URL = "https://atelier.mananjoshi.me/api";
22
+ var DEFAULT_GITHUB_CLIENT_ID = "Ov23liiscZlMXcJ2RLnd";
23
+ function getConfiguredApiUrl() {
24
+ return process.env.ATELIER_API_URL ?? DEFAULT_API_URL;
25
+ }
26
+ function getConfiguredGithubClientId() {
27
+ return process.env.ATELIER_GITHUB_CLIENT_ID ?? DEFAULT_GITHUB_CLIENT_ID;
28
+ }
29
+ async function loginWithGitHub(options) {
30
+ const apiUrl = options.apiUrl ?? getConfiguredApiUrl();
31
+ const clientId = options.clientId ?? getConfiguredGithubClientId();
32
+ const device = await requestDeviceCode(clientId);
33
+ await options.onPrompt({
34
+ verificationUri: device.verification_uri,
35
+ userCode: device.user_code,
36
+ expiresIn: device.expires_in
37
+ });
38
+ const githubAccessToken = await pollForGitHubAccessToken(clientId, device, options.onPoll);
39
+ const exchange = await fetch(`${apiUrl}/auth/github/exchange`, {
40
+ method: "POST",
41
+ headers: { "content-type": "application/json" },
42
+ body: JSON.stringify({ githubAccessToken })
43
+ });
44
+ if (!exchange.ok) {
45
+ throw new Error(`Atelier API rejected GitHub login (${exchange.status})`);
46
+ }
47
+ const body = await exchange.json();
48
+ saveSession({ apiUrl, user: body.user, token: body.token });
49
+ return { apiUrl, user: body.user };
50
+ }
51
+ async function getCurrentUser() {
52
+ const authenticated = getAuthenticatedSession();
53
+ if (!authenticated)
54
+ return;
55
+ const response = await fetch(`${authenticated.apiUrl}/me`, {
56
+ headers: { authorization: `Bearer ${authenticated.token}` }
57
+ });
58
+ if (!response.ok)
59
+ return;
60
+ const body = await response.json();
61
+ return { user: body.user, apiUrl: authenticated.apiUrl };
62
+ }
63
+ function getAuthenticatedSession() {
64
+ const session = readSession();
65
+ if (!session)
66
+ return;
67
+ const token = readSessionToken(session);
68
+ if (!token)
69
+ return;
70
+ return { apiUrl: session.apiUrl, user: session.user, token };
71
+ }
72
+ function getActiveProfile() {
73
+ return readConfig().activeProfile ?? "personal";
74
+ }
75
+ function setActiveProfile(profile) {
76
+ writeConfig({ ...readConfig(), activeProfile: profile });
77
+ }
78
+ function logout() {
79
+ const session = readSession();
80
+ if (session?.tokenStorage === "keychain")
81
+ deleteKeychainToken(session.user.id);
82
+ if (existsSync(SESSION_PATH))
83
+ rmSync(SESSION_PATH);
84
+ }
85
+ function readSession() {
86
+ if (!existsSync(SESSION_PATH))
87
+ return;
88
+ return JSON.parse(readFileSync(SESSION_PATH, "utf-8"));
89
+ }
90
+ function readConfig() {
91
+ if (!existsSync(CONFIG_PATH))
92
+ return {};
93
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
94
+ }
95
+ function writeConfig(config) {
96
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
97
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
98
+ `, "utf-8");
99
+ chmodSync(CONFIG_PATH, 384);
100
+ }
101
+ function saveSession(input) {
102
+ mkdirSync(dirname(SESSION_PATH), { recursive: true });
103
+ const canUseKeychain = platform() === "darwin";
104
+ const session = {
105
+ apiUrl: input.apiUrl,
106
+ user: input.user,
107
+ tokenStorage: canUseKeychain ? "keychain" : "file",
108
+ createdAt: new Date().toISOString()
109
+ };
110
+ if (canUseKeychain) {
111
+ saveKeychainToken(input.user.id, input.token);
112
+ } else {
113
+ session.token = input.token;
114
+ }
115
+ writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + `
116
+ `, "utf-8");
117
+ chmodSync(SESSION_PATH, 384);
118
+ }
119
+ function readSessionToken(session) {
120
+ if (session.tokenStorage === "file")
121
+ return session.token;
122
+ return readKeychainToken(session.user.id);
123
+ }
124
+ async function requestDeviceCode(clientId) {
125
+ const response = await fetch("https://github.com/login/device/code", {
126
+ method: "POST",
127
+ headers: { accept: "application/json", "content-type": "application/json" },
128
+ body: JSON.stringify({ client_id: clientId, scope: "read:user user:email" })
129
+ });
130
+ if (!response.ok)
131
+ throw new Error(`GitHub device-code request failed (${response.status})`);
132
+ return response.json();
133
+ }
134
+ async function pollForGitHubAccessToken(clientId, device, onPoll) {
135
+ let interval = device.interval;
136
+ const expiresAt = Date.now() + device.expires_in * 1000;
137
+ while (Date.now() < expiresAt) {
138
+ await sleep(interval * 1000);
139
+ await onPoll?.("Waiting for GitHub authorization\u2026");
140
+ const response = await fetch("https://github.com/login/oauth/access_token", {
141
+ method: "POST",
142
+ headers: { accept: "application/json", "content-type": "application/json" },
143
+ body: JSON.stringify({ client_id: clientId, device_code: device.device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code" })
144
+ });
145
+ if (!response.ok)
146
+ throw new Error(`GitHub token polling failed (${response.status})`);
147
+ const body = await response.json();
148
+ if (body.access_token)
149
+ return body.access_token;
150
+ if (body.error === "authorization_pending")
151
+ continue;
152
+ if (body.error === "slow_down") {
153
+ interval += 5;
154
+ continue;
155
+ }
156
+ if (body.error === "access_denied")
157
+ throw new Error("GitHub login was denied");
158
+ if (body.error === "expired_token")
159
+ throw new Error("GitHub login code expired");
160
+ throw new Error(body.error_description ?? "GitHub login failed");
161
+ }
162
+ throw new Error("GitHub login timed out");
163
+ }
164
+ function saveKeychainToken(account, token) {
165
+ execFileSync("security", ["add-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE, "-w", token, "-U"], { stdio: "ignore" });
166
+ }
167
+ function readKeychainToken(account) {
168
+ try {
169
+ return execFileSync("security", ["find-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE, "-w"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
170
+ } catch {
171
+ return;
172
+ }
173
+ }
174
+ function deleteKeychainToken(account) {
175
+ try {
176
+ execFileSync("security", ["delete-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE], { stdio: "ignore" });
177
+ } catch {}
178
+ }
179
+ function sleep(ms) {
180
+ return new Promise((resolve) => setTimeout(resolve, ms));
181
+ }
182
+
183
+ // ../../packages/api-client/src/index.ts
184
+ async function listProfiles() {
185
+ const response = await authedFetch("/profiles");
186
+ const body = await response.json();
187
+ return body.profiles;
188
+ }
189
+ async function createProfile(name) {
190
+ const response = await authedFetch("/profiles", {
191
+ method: "POST",
192
+ headers: { "content-type": "application/json" },
193
+ body: JSON.stringify({ name })
194
+ });
195
+ const body = await response.json();
196
+ return body.profile;
197
+ }
198
+ async function getProfile(name) {
199
+ const response = await authedFetch(`/profiles/${encodeURIComponent(name)}`);
200
+ if (response.status === 404)
201
+ return;
202
+ const body = await response.json();
203
+ return body.profile;
204
+ }
205
+ async function getVault() {
206
+ return await (await authedFetch("/vault")).json();
207
+ }
208
+ async function saveVault(input) {
209
+ await authedFetch("/vault", {
210
+ method: "PUT",
211
+ headers: { "content-type": "application/json" },
212
+ body: JSON.stringify(input)
213
+ });
214
+ }
215
+ async function getProfileKey(profileName) {
216
+ const response = await authedFetch(`/profiles/${encodeURIComponent(profileName)}/key`);
217
+ if (response.status === 404)
218
+ return;
219
+ const body = await response.json();
220
+ return body.profileKey;
221
+ }
222
+ async function saveProfileKey(profileName, input) {
223
+ const response = await authedFetch(`/profiles/${encodeURIComponent(profileName)}/key`, {
224
+ method: "PUT",
225
+ headers: { "content-type": "application/json" },
226
+ body: JSON.stringify(input)
227
+ });
228
+ const body = await response.json();
229
+ return body.profileKey;
230
+ }
231
+ async function listSavedConfigs(profileName) {
232
+ const response = await authedFetch(`/profiles/${encodeURIComponent(profileName)}/configs`);
233
+ const body = await response.json();
234
+ return body.configs;
235
+ }
236
+ async function getSavedConfig(profileName, stableId) {
237
+ const response = await authedFetch(`/profiles/${encodeURIComponent(profileName)}/configs/${encodeURIComponent(stableId)}`);
238
+ if (response.status === 404)
239
+ return;
240
+ const body = await response.json();
241
+ return body.config;
242
+ }
243
+ async function createConfigVersion(profileName, stableId, input) {
244
+ const response = await authedFetch(`/profiles/${encodeURIComponent(profileName)}/configs/${encodeURIComponent(stableId)}/versions`, {
245
+ method: "POST",
246
+ headers: { "content-type": "application/json" },
247
+ body: JSON.stringify(input)
248
+ });
249
+ return await response.json();
250
+ }
251
+ async function authedFetch(path, init = {}) {
252
+ const session = getAuthenticatedSession();
253
+ if (!session)
254
+ throw new Error("Not logged in. Run `atl login`.");
255
+ const response = await fetch(`${session.apiUrl}${path}`, {
256
+ ...init,
257
+ headers: {
258
+ ...init.headers,
259
+ authorization: `Bearer ${session.token}`
260
+ }
261
+ });
262
+ if (!response.ok && response.status !== 404) {
263
+ const body = await response.json().catch(() => {
264
+ return;
265
+ });
266
+ throw new Error(body?.error ?? `Atelier API request failed (${response.status})`);
267
+ }
268
+ return response;
269
+ }
270
+
271
+ // ../../packages/save-client/src/index.ts
272
+ import { createCipheriv as createCipheriv2, createHash, randomBytes as randomBytes2 } from "crypto";
273
+ import { readFile } from "fs/promises";
274
+
275
+ // ../../packages/vault-client/src/index.ts
276
+ import { execFileSync as execFileSync2 } from "child_process";
277
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
278
+ import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
279
+ import { homedir as homedir2, platform as platform2 } from "os";
280
+ import { dirname as dirname2, join as join2 } from "path";
281
+ var VAULT_KEYCHAIN_SERVICE = "dev.atelier.vault";
282
+ var LOCAL_VAULT_DIR = join2(homedir2(), ".config", "atelier");
283
+ var LOCAL_VAULT_KEY_PATH = join2(LOCAL_VAULT_DIR, "vault-key.json");
284
+ var KDF_PARAMS = { N: 32768, r: 8, p: 1 };
285
+ var KDF_MAXMEM = 64 * 1024 * 1024;
286
+ var KEY_ALGORITHM = "aes-256-gcm";
287
+ async function getVaultState() {
288
+ const session = requireSession();
289
+ const vault = await getVault();
290
+ return {
291
+ initialized: vault.initialized,
292
+ unlocked: Boolean(readLocalVaultKey(session.user.id)),
293
+ activeProfile: getActiveProfile()
294
+ };
295
+ }
296
+ async function initializeVault(passphrase, profileName = getActiveProfile()) {
297
+ assertStrongPassphrase(passphrase);
298
+ const session = requireSession();
299
+ const existing = await getVault();
300
+ if (existing.initialized)
301
+ throw new Error("Atelier vault already exists. Run `atl vault unlock` if this machine is locked.");
302
+ const profile = await getProfile(profileName);
303
+ if (!profile)
304
+ throw new Error(`Profile not found: ${profileName}`);
305
+ const vaultKey = randomBytes(32);
306
+ const profileKey = randomBytes(32);
307
+ const salt = randomBytes(16);
308
+ const wrappingKey = deriveWrappingKey(passphrase, salt, KDF_PARAMS);
309
+ const encryptedVaultKey = encryptBytes(vaultKey, wrappingKey);
310
+ const encryptedProfileKey = encryptBytes(profileKey, vaultKey);
311
+ await saveVault({
312
+ kdf: {
313
+ algorithm: "scrypt",
314
+ salt: base64urlEncode(salt),
315
+ params: KDF_PARAMS
316
+ },
317
+ encryptedVaultKey
318
+ });
319
+ await saveProfileKey(profileName, {
320
+ version: 1,
321
+ algorithm: encryptedProfileKey.algorithm,
322
+ nonce: encryptedProfileKey.nonce,
323
+ encryptedKey: encryptedProfileKey.ciphertext
324
+ });
325
+ saveLocalVaultKey(session.user.id, vaultKey);
326
+ return { profileName: profile.name };
327
+ }
328
+ async function unlockVault(passphrase) {
329
+ const session = requireSession();
330
+ const vault = await getVault();
331
+ if (!vault.initialized)
332
+ throw new Error("Atelier vault is not initialized. Run `atl save <id>` or `atl vault init`.");
333
+ const vaultKey = decryptVaultKey(vault, passphrase);
334
+ saveLocalVaultKey(session.user.id, vaultKey);
335
+ return { activeProfile: getActiveProfile() };
336
+ }
337
+ function lockVault() {
338
+ const session = requireSession();
339
+ deleteLocalVaultKey(session.user.id);
340
+ }
341
+ async function ensureLocalVaultKey(passphrase, profileName = getActiveProfile()) {
342
+ const session = requireSession();
343
+ const localVaultKey = readLocalVaultKey(session.user.id);
344
+ if (localVaultKey)
345
+ return localVaultKey;
346
+ if (!passphrase)
347
+ throw new Error("Vault passphrase is required");
348
+ const vault = await getVault();
349
+ if (!vault.initialized) {
350
+ await initializeVault(passphrase, profileName);
351
+ const initializedKey = readLocalVaultKey(session.user.id);
352
+ if (!initializedKey)
353
+ throw new Error("Vault initialized but local key was not stored");
354
+ return initializedKey;
355
+ }
356
+ return decryptVaultKey(vault, passphrase);
357
+ }
358
+ async function ensureProfileKey(passphrase, profileName = getActiveProfile()) {
359
+ const vaultKey = await ensureLocalVaultKey(passphrase, profileName);
360
+ const existing = await getProfileKey(profileName);
361
+ if (existing)
362
+ return decryptEnvelope(existing.encryptedKey, existing.nonce, vaultKey);
363
+ const profileKey = randomBytes(32);
364
+ const encryptedProfileKey = encryptBytes(profileKey, vaultKey);
365
+ await saveProfileKey(profileName, {
366
+ version: 1,
367
+ algorithm: encryptedProfileKey.algorithm,
368
+ nonce: encryptedProfileKey.nonce,
369
+ encryptedKey: encryptedProfileKey.ciphertext
370
+ });
371
+ return profileKey;
372
+ }
373
+ function decryptVaultKey(vault, passphrase) {
374
+ if (!vault.kdf || !vault.encryptedVaultKey)
375
+ throw new Error("Vault payload is incomplete");
376
+ if (vault.kdf.algorithm !== "scrypt")
377
+ throw new Error(`Unsupported vault KDF: ${vault.kdf.algorithm}`);
378
+ const salt = base64urlDecode(vault.kdf.salt);
379
+ const wrappingKey = deriveWrappingKey(passphrase, salt, vault.kdf.params);
380
+ const vaultKey = decryptEnvelope(vault.encryptedVaultKey.ciphertext, vault.encryptedVaultKey.nonce, wrappingKey);
381
+ saveLocalVaultKey(requireSession().user.id, vaultKey);
382
+ return vaultKey;
383
+ }
384
+ function deriveWrappingKey(passphrase, salt, params) {
385
+ return scryptSync(passphrase, salt, 32, {
386
+ N: params.N,
387
+ r: params.r,
388
+ p: params.p,
389
+ maxmem: KDF_MAXMEM
390
+ });
391
+ }
392
+ function encryptBytes(plaintext, key) {
393
+ const nonce = randomBytes(12);
394
+ const cipher = createCipheriv(KEY_ALGORITHM, key, nonce);
395
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
396
+ const tag = cipher.getAuthTag();
397
+ return {
398
+ algorithm: KEY_ALGORITHM,
399
+ nonce: base64urlEncode(nonce),
400
+ ciphertext: base64urlEncode(Buffer.concat([ciphertext, tag]))
401
+ };
402
+ }
403
+ function decryptEnvelope(ciphertextWithTag, nonceValue, key) {
404
+ const payload = Buffer.from(base64urlDecode(ciphertextWithTag));
405
+ const nonce = Buffer.from(base64urlDecode(nonceValue));
406
+ const ciphertext = payload.subarray(0, -16);
407
+ const tag = payload.subarray(-16);
408
+ const decipher = createDecipheriv(KEY_ALGORITHM, key, nonce);
409
+ decipher.setAuthTag(tag);
410
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
411
+ }
412
+ function assertStrongPassphrase(passphrase) {
413
+ if (passphrase.length < 12)
414
+ throw new Error("Vault passphrase must be at least 12 characters.");
415
+ }
416
+ function requireSession() {
417
+ const session = getAuthenticatedSession();
418
+ if (!session)
419
+ throw new Error("Not logged in. Run `atl login`.");
420
+ return session;
421
+ }
422
+ function saveLocalVaultKey(userId, key) {
423
+ const encoded = base64urlEncode(key);
424
+ if (platform2() === "darwin") {
425
+ execFileSync2("security", ["add-generic-password", "-a", userId, "-s", VAULT_KEYCHAIN_SERVICE, "-w", encoded, "-U"], { stdio: "ignore" });
426
+ return;
427
+ }
428
+ mkdirSync2(dirname2(LOCAL_VAULT_KEY_PATH), { recursive: true });
429
+ writeFileSync2(LOCAL_VAULT_KEY_PATH, JSON.stringify({ userId, key: encoded }, null, 2) + `
430
+ `, "utf-8");
431
+ chmodSync2(LOCAL_VAULT_KEY_PATH, 384);
432
+ }
433
+ function readLocalVaultKey(userId) {
434
+ const encoded = platform2() === "darwin" ? readKeychainVaultKey(userId) : readFileVaultKey(userId);
435
+ return encoded ? Buffer.from(base64urlDecode(encoded)) : undefined;
436
+ }
437
+ function deleteLocalVaultKey(userId) {
438
+ if (platform2() === "darwin") {
439
+ try {
440
+ execFileSync2("security", ["delete-generic-password", "-a", userId, "-s", VAULT_KEYCHAIN_SERVICE], { stdio: "ignore" });
441
+ } catch {}
442
+ return;
443
+ }
444
+ if (existsSync2(LOCAL_VAULT_KEY_PATH))
445
+ rmSync2(LOCAL_VAULT_KEY_PATH);
446
+ }
447
+ function readKeychainVaultKey(userId) {
448
+ try {
449
+ return execFileSync2("security", ["find-generic-password", "-a", userId, "-s", VAULT_KEYCHAIN_SERVICE, "-w"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
450
+ } catch {
451
+ return;
452
+ }
453
+ }
454
+ function readFileVaultKey(userId) {
455
+ if (!existsSync2(LOCAL_VAULT_KEY_PATH))
456
+ return;
457
+ const body = JSON.parse(readFileSync2(LOCAL_VAULT_KEY_PATH, "utf-8"));
458
+ return body.userId === userId ? body.key : undefined;
459
+ }
460
+ function base64urlEncode(bytes) {
461
+ return Buffer.from(bytes).toString("base64url");
462
+ }
463
+ function base64urlDecode(value) {
464
+ return new Uint8Array(Buffer.from(value, "base64url"));
465
+ }
466
+
467
+ // ../../packages/save-client/src/validation.ts
468
+ var MAX_SAVE_SIZE_BYTES = 1024 * 1024;
469
+ function validateSaveCandidate(item) {
470
+ const reasons = [];
471
+ if (!item.exists)
472
+ reasons.push("file does not exist");
473
+ if (item.isDirectory || item.kind === "directory" || item.format === "directory")
474
+ reasons.push("directory saving is not supported yet");
475
+ if (item.kind === "generated")
476
+ reasons.push("generated/cache files are not saved");
477
+ if (item.kind === "private" || item.shareability === "private")
478
+ reasons.push("private/auth files are blocked");
479
+ if (item.secretFindings.length > 0)
480
+ reasons.push("detected secret material");
481
+ if (item.size !== undefined && item.size > MAX_SAVE_SIZE_BYTES)
482
+ reasons.push(`file is larger than ${formatBytes(MAX_SAVE_SIZE_BYTES)}`);
483
+ if (isUnsupportedBinary(item))
484
+ reasons.push("binary files are not supported yet");
485
+ return { ok: reasons.length === 0, reasons };
486
+ }
487
+ function assertSaveCandidate(item) {
488
+ const result = validateSaveCandidate(item);
489
+ if (!result.ok)
490
+ throw new Error(`Cannot save ${item.id}: ${result.reasons.join("; ")}`);
491
+ }
492
+ function isUnsupportedBinary(item) {
493
+ if (item.previewSuppressedReason?.toLowerCase().includes("binary"))
494
+ return true;
495
+ return ["binary", "sqlite", "db"].includes(item.format.toLowerCase());
496
+ }
497
+ function formatBytes(bytes) {
498
+ if (bytes < 1024)
499
+ return `${bytes} B`;
500
+ const kib = bytes / 1024;
501
+ if (kib < 1024)
502
+ return `${Math.round(kib)} KiB`;
503
+ return `${Math.round(kib / 1024)} MiB`;
504
+ }
505
+ // ../../packages/save-client/src/index.ts
506
+ var CONTENT_ALGORITHM = "aes-256-gcm";
507
+ async function saveConfigItem(item, options = {}) {
508
+ assertSaveCandidate(item);
509
+ const profileName = getActiveProfile();
510
+ const profileKey = await ensureProfileKey(options.passphrase, profileName);
511
+ const plaintext = await readFile(item.path);
512
+ const encrypted = encryptContent(plaintext, profileKey);
513
+ const response = await createConfigVersion(profileName, item.id, {
514
+ kind: "file",
515
+ pathHint: item.displayPath,
516
+ contentSha256: sha256Hex(plaintext),
517
+ ciphertextSha256: sha256Hex(encrypted.ciphertextBytes),
518
+ sizeBytes: plaintext.byteLength,
519
+ algorithm: encrypted.algorithm,
520
+ profileKeyVersion: 1,
521
+ nonce: encrypted.nonce,
522
+ ciphertext: encrypted.ciphertext
523
+ });
524
+ return {
525
+ profileName,
526
+ stableId: item.id,
527
+ version: response.version,
528
+ reused: response.reused
529
+ };
530
+ }
531
+ function encryptContent(plaintext, key) {
532
+ const nonce = randomBytes2(12);
533
+ const cipher = createCipheriv2(CONTENT_ALGORITHM, key, nonce);
534
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
535
+ const tag = cipher.getAuthTag();
536
+ const ciphertextBytes = Buffer.concat([ciphertext, tag]);
537
+ return {
538
+ algorithm: CONTENT_ALGORITHM,
539
+ nonce: base64urlEncode2(nonce),
540
+ ciphertext: base64urlEncode2(ciphertextBytes),
541
+ ciphertextBytes
542
+ };
543
+ }
544
+ function sha256Hex(bytes) {
545
+ return createHash("sha256").update(bytes).digest("hex");
546
+ }
547
+ function base64urlEncode2(bytes) {
548
+ return Buffer.from(bytes).toString("base64url");
549
+ }
550
+
551
+ // ../../packages/scanner/src/index.ts
552
+ import { execFileSync as execFileSync3 } from "child_process";
553
+ import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
554
+ import { homedir as homedir3, userInfo } from "os";
555
+ import { dirname as dirname3, join as join3, relative, resolve } from "path";
556
+
557
+ // ../../packages/registry/src/definitions.ts
558
+ var registryDefinitions = [
559
+ {
560
+ domain: "AI Tools",
561
+ app: "Pi / Pi Next",
562
+ items: [
563
+ {
564
+ id: "pi.dir",
565
+ path: "~/.pi",
566
+ kind: "directory",
567
+ format: "directory",
568
+ shareability: "machine-specific"
569
+ },
570
+ {
571
+ id: "pi-next.dir",
572
+ path: "~/.pi-next",
573
+ kind: "directory",
574
+ format: "directory",
575
+ shareability: "shareable"
576
+ },
577
+ {
578
+ id: "pi-next.design",
579
+ path: "~/.pi-next/DESIGN.md",
580
+ kind: "config",
581
+ format: "markdown",
582
+ shareability: "shareable"
583
+ },
584
+ {
585
+ id: "pi-next.cmux-config",
586
+ path: "~/.pi-next/cmux-config",
587
+ kind: "config",
588
+ format: "directory",
589
+ shareability: "shareable"
590
+ },
591
+ {
592
+ id: "pi-next.zed-config",
593
+ path: "~/.pi-next/zed-config",
594
+ kind: "config",
595
+ format: "directory",
596
+ shareability: "shareable"
597
+ },
598
+ {
599
+ id: "pi-next.bin",
600
+ path: "~/.pi-next/bin",
601
+ kind: "config",
602
+ format: "directory",
603
+ shareability: "shareable"
604
+ }
605
+ ]
606
+ },
607
+ {
608
+ domain: "Shell",
609
+ app: "Starship",
610
+ items: [
611
+ {
612
+ id: "starship.config",
613
+ path: "~/.config/starship.toml",
614
+ kind: "config",
615
+ format: "toml",
616
+ shareability: "shareable"
617
+ }
618
+ ]
619
+ },
620
+ {
621
+ domain: "Editors",
622
+ app: "Zed",
623
+ items: [
624
+ {
625
+ id: "zed.dir",
626
+ path: "~/.config/zed",
627
+ kind: "directory",
628
+ format: "directory",
629
+ shareability: "shareable"
630
+ },
631
+ {
632
+ id: "zed.settings",
633
+ path: "~/.config/zed/settings.json",
634
+ kind: "config",
635
+ format: "jsonc",
636
+ shareability: "shareable",
637
+ mirrors: [
638
+ "~/.pi-next/zed-config/settings.json"
639
+ ]
640
+ },
641
+ {
642
+ id: "zed.themes",
643
+ path: "~/.config/zed/themes",
644
+ kind: "config",
645
+ format: "directory",
646
+ shareability: "shareable"
647
+ },
648
+ {
649
+ id: "zed.prompts",
650
+ path: "~/.config/zed/prompts",
651
+ kind: "generated",
652
+ format: "directory",
653
+ shareability: "machine-specific"
654
+ }
655
+ ]
656
+ },
657
+ {
658
+ domain: "Git",
659
+ app: "Git",
660
+ items: [
661
+ {
662
+ id: "git.config",
663
+ path: "~/.gitconfig",
664
+ kind: "config",
665
+ format: "gitconfig",
666
+ shareability: "machine-specific"
667
+ },
668
+ {
669
+ id: "git.config-dir",
670
+ path: "~/.config/git",
671
+ kind: "directory",
672
+ format: "directory",
673
+ shareability: "machine-specific"
674
+ },
675
+ {
676
+ id: "git.ignore",
677
+ path: "~/.gitignore",
678
+ kind: "config",
679
+ format: "gitignore",
680
+ shareability: "machine-specific"
681
+ }
682
+ ]
683
+ },
684
+ {
685
+ domain: "Private/Auth",
686
+ app: "Credentials",
687
+ privateByDefault: true,
688
+ items: [
689
+ {
690
+ id: "auth.ssh",
691
+ path: "~/.ssh",
692
+ kind: "private",
693
+ format: "directory",
694
+ shareability: "private"
695
+ },
696
+ {
697
+ id: "auth.aws",
698
+ path: "~/.aws",
699
+ kind: "private",
700
+ format: "directory",
701
+ shareability: "private"
702
+ },
703
+ {
704
+ id: "auth.gh",
705
+ path: "~/.config/gh",
706
+ kind: "private",
707
+ format: "directory",
708
+ shareability: "private"
709
+ },
710
+ {
711
+ id: "auth.gcloud-adc",
712
+ path: "~/.config/gcloud/application_default_credentials.json",
713
+ kind: "private",
714
+ format: "json",
715
+ shareability: "private"
716
+ },
717
+ {
718
+ id: "auth.docker",
719
+ path: "~/.docker/config.json",
720
+ kind: "private",
721
+ format: "json",
722
+ shareability: "private"
723
+ },
724
+ {
725
+ id: "auth.pi",
726
+ path: "~/.config/pi/auth.json",
727
+ kind: "private",
728
+ format: "json",
729
+ shareability: "private"
730
+ }
731
+ ]
732
+ },
733
+ {
734
+ domain: "Shell",
735
+ app: "Zsh",
736
+ items: [
737
+ {
738
+ id: "zsh.home-rc",
739
+ path: "~/.zshrc",
740
+ kind: "config",
741
+ format: "shell",
742
+ shareability: "shareable"
743
+ },
744
+ {
745
+ id: "zsh.home-env",
746
+ path: "~/.zshenv",
747
+ kind: "config",
748
+ format: "shell",
749
+ shareability: "private"
750
+ },
751
+ {
752
+ id: "zsh.dir",
753
+ path: "~/.config/zsh",
754
+ kind: "directory",
755
+ format: "directory",
756
+ shareability: "machine-specific"
757
+ },
758
+ {
759
+ id: "zsh.env",
760
+ path: "~/.config/zsh/.zshenv",
761
+ kind: "config",
762
+ format: "shell",
763
+ shareability: "private"
764
+ },
765
+ {
766
+ id: "zsh.rc",
767
+ path: "~/.config/zsh/.zshrc",
768
+ kind: "config",
769
+ format: "shell",
770
+ shareability: "shareable"
771
+ },
772
+ {
773
+ id: "zsh.conf",
774
+ path: "~/.config/zsh/conf.d",
775
+ kind: "directory",
776
+ format: "directory",
777
+ shareability: "shareable"
778
+ },
779
+ {
780
+ id: "zsh.zcompdump",
781
+ path: "~/.config/zsh/.zcompdump",
782
+ kind: "generated",
783
+ format: "text",
784
+ shareability: "machine-specific"
785
+ },
786
+ {
787
+ id: "zsh.sessions",
788
+ path: "~/.config/zsh/.zsh_sessions",
789
+ kind: "generated",
790
+ format: "directory",
791
+ shareability: "machine-specific"
792
+ }
793
+ ]
794
+ },
795
+ {
796
+ domain: "Terminals",
797
+ app: "cmux",
798
+ items: [
799
+ {
800
+ id: "cmux.dir",
801
+ path: "~/.config/cmux",
802
+ kind: "directory",
803
+ format: "directory",
804
+ shareability: "shareable"
805
+ },
806
+ {
807
+ id: "cmux.config",
808
+ path: "~/.config/cmux/cmux.json",
809
+ kind: "config",
810
+ format: "jsonc",
811
+ shareability: "shareable",
812
+ mirrors: [
813
+ "~/.pi-next/cmux-config/cmux.json"
814
+ ]
815
+ },
816
+ {
817
+ id: "cmux.ghostty",
818
+ path: "~/Library/Application Support/com.cmuxterm.app/config.ghostty",
819
+ kind: "config",
820
+ format: "ghostty",
821
+ shareability: "shareable",
822
+ mirrors: [
823
+ "~/.pi-next/cmux-config/config.ghostty"
824
+ ]
825
+ },
826
+ {
827
+ id: "cmux.browser-history",
828
+ path: "~/Library/Application Support/com.cmuxterm.app/browser_history.json",
829
+ kind: "generated",
830
+ format: "json",
831
+ shareability: "machine-specific"
832
+ }
833
+ ]
834
+ }
835
+ ];
836
+
837
+ // ../../packages/secrets/src/index.ts
838
+ var SECRET_PATTERNS = [
839
+ ["GitHub token", /gh[pousr]_[A-Za-z0-9_]{20,}/g],
840
+ ["Slack token", /xox[baprs]-[A-Za-z0-9-]{20,}/g],
841
+ ["AWS access key", /AKIA[0-9A-Z]{16}/g],
842
+ ["Private key", /-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/g],
843
+ ["Generic token assignment", /(?:token|api[_-]?key|secret|webhook|password)\s*[=:]\s*["']?[^"'\s]{12,}/gi]
844
+ ];
845
+ function detectSecrets(text) {
846
+ const findings = [];
847
+ const lines = text.split(/\r?\n/);
848
+ for (const [type, pattern] of SECRET_PATTERNS) {
849
+ for (const match of text.matchAll(pattern)) {
850
+ const index = match.index ?? 0;
851
+ const line = text.slice(0, index).split(/\r?\n/).length;
852
+ findings.push({ type, line, preview: redact(match[0]) });
853
+ }
854
+ }
855
+ lines.forEach((lineText, index) => {
856
+ for (const candidate of lineText.matchAll(/[A-Za-z0-9_+\/=.-]{32,}/g)) {
857
+ const value = candidate[0];
858
+ if (!isCoveredByExplicitPattern(value, lineText) && isHighEntropySecretCandidate(value, lineText)) {
859
+ findings.push({ type: "High-entropy string", line: index + 1, preview: redact(value) });
860
+ }
861
+ }
862
+ });
863
+ return dedupe(findings);
864
+ }
865
+ function redact(text) {
866
+ if (text.length <= 8)
867
+ return "[redacted]";
868
+ return `${text.slice(0, 4)}\u2026${text.slice(-4)}`;
869
+ }
870
+ function redactText(text) {
871
+ let redacted = text;
872
+ for (const [, pattern] of SECRET_PATTERNS) {
873
+ redacted = redacted.replace(pattern, (match) => redact(match));
874
+ }
875
+ redacted = redacted.replace(/[A-Za-z0-9_+\/=.-]{32,}/g, (match) => {
876
+ if (isHighEntropySecretCandidate(match, text))
877
+ return redact(match);
878
+ return match;
879
+ });
880
+ return redacted;
881
+ }
882
+ function isCoveredByExplicitPattern(value, context) {
883
+ return SECRET_PATTERNS.some(([, pattern]) => {
884
+ pattern.lastIndex = 0;
885
+ return Array.from(context.matchAll(pattern)).some((match) => match[0].includes(value) || value.includes(match[0]));
886
+ });
887
+ }
888
+ function isHighEntropySecretCandidate(value, context) {
889
+ if (value.startsWith("amazon."))
890
+ return false;
891
+ if (value.includes("anthropic.claude"))
892
+ return false;
893
+ if (/^[A-Za-z0-9.-]+@[A-Za-z0-9.-]+$/.test(value))
894
+ return false;
895
+ if (/model/i.test(context) && /^[A-Za-z0-9_.-]+$/.test(value))
896
+ return false;
897
+ return entropy(value) > 4.2 && /[A-Za-z]/.test(value) && /[0-9]/.test(value);
898
+ }
899
+ function entropy(value) {
900
+ const counts = new Map;
901
+ for (const char of value)
902
+ counts.set(char, (counts.get(char) ?? 0) + 1);
903
+ let result = 0;
904
+ for (const count of counts.values()) {
905
+ const p = count / value.length;
906
+ result -= p * Math.log2(p);
907
+ }
908
+ return result;
909
+ }
910
+ function dedupe(findings) {
911
+ const seen = new Set;
912
+ return findings.filter((finding) => {
913
+ const key = `${finding.type}:${finding.line}:${finding.preview}`;
914
+ if (seen.has(key))
915
+ return false;
916
+ seen.add(key);
917
+ return true;
918
+ });
919
+ }
920
+
921
+ // ../../packages/scanner/src/jsonc.ts
922
+ function parseJsonc(input) {
923
+ const withoutBlockComments = input.replace(/\/\*[\s\S]*?\*\//g, "");
924
+ const withoutLineComments = withoutBlockComments.replace(/(^|[^:])\/\/.*$/gm, "$1");
925
+ const withoutTrailingCommas = withoutLineComments.replace(/,\s*([}\]])/g, "$1");
926
+ return JSON.parse(withoutTrailingCommas);
927
+ }
928
+
929
+ // ../../packages/scanner/src/index.ts
930
+ var HOME = homedir3();
931
+ var TEXT_EXTENSIONS = new Set([".zsh", ".zshrc", ".zshenv", ".sh", ".json", ".jsonc", ".toml", ".md", ".yml", ".yaml", ".gitconfig", ".gitignore", ".ghostty", ""]);
932
+ var GENERATED_NAMES = [/cache/i, /history/i, /session/i, /state/i, /\.mdb$/i, /\.sqlite/i, /\.db$/i, /zcompdump/i, /logs?/i];
933
+ var PRIVATE_NAMES = [/credential/i, /secret/i, /token/i, /auth\.json$/i, /hosts\.yml$/i, /config\.json$/i];
934
+ var yadmCache;
935
+ async function scan(options) {
936
+ yadmCache = options.includeLegacyManagers ? loadYadmCache() : undefined;
937
+ const definitions = loadRegistry(options.repoRoot);
938
+ const registryItems = definitions.flatMap((definition) => definition.items.map((item) => ({ definition, item })));
939
+ const seen = new Set;
940
+ const items = [];
941
+ for (const { definition, item } of registryItems) {
942
+ const path = expandHome(item.path);
943
+ seen.add(path);
944
+ items.push(inspectRegistryItem(definition, item, path));
945
+ }
946
+ for (const candidate of discoverConfigCandidates()) {
947
+ if (seen.has(candidate.path))
948
+ continue;
949
+ seen.add(candidate.path);
950
+ items.push(inspectHeuristicItem(candidate));
951
+ }
952
+ for (const manual of options.manualPaths ?? []) {
953
+ const path = expandHome(manual);
954
+ if (seen.has(path))
955
+ continue;
956
+ seen.add(path);
957
+ items.push(inspectHeuristicItem({ path, reason: "manual path" }));
958
+ }
959
+ items.sort((a, b) => `${a.domain}:${a.app}:${a.path}`.localeCompare(`${b.domain}:${b.app}:${b.path}`));
960
+ return {
961
+ version: 1,
962
+ generatedAt: new Date().toISOString(),
963
+ repoRoot: options.repoRoot,
964
+ home: HOME,
965
+ items,
966
+ summary: {
967
+ total: items.length,
968
+ existing: items.filter((item) => item.exists).length,
969
+ secrets: items.filter((item) => item.secretFindings.length > 0).length,
970
+ drift: items.filter((item) => item.mirrors.some((mirror) => mirror.exists && mirror.identical === false)).length,
971
+ generated: items.filter((item) => item.kind === "generated").length,
972
+ private: items.filter((item) => item.shareability === "private").length
973
+ }
974
+ };
975
+ }
976
+ function writeInventory(inventory, options = {}) {
977
+ const path = options.path ?? join3(inventory.repoRoot, ".atelier", "state", "inventory.json");
978
+ mkdirSync3(dirname3(path), { recursive: true });
979
+ writeFileSync3(path, JSON.stringify(inventory, null, 2) + `
980
+ `, "utf-8");
981
+ return path;
982
+ }
983
+ function loadRegistry(repoRoot) {
984
+ const dir = join3(repoRoot, "packages", "registry", "definitions");
985
+ if (!existsSync3(dir))
986
+ return registryDefinitions;
987
+ return readdirSync(dir).filter((file) => file.endsWith(".jsonc")).map((file) => parseJsonc(readFileSync3(join3(dir, file), "utf-8")));
988
+ }
989
+ function inspectRegistryItem(definition, item, path) {
990
+ return inspectPath({
991
+ path,
992
+ domain: definition.domain,
993
+ app: definition.app,
994
+ kind: item.kind,
995
+ format: item.format,
996
+ shareability: item.shareability,
997
+ reason: "registry match",
998
+ mirrors: item.mirrors ?? [],
999
+ privateByDefault: definition.privateByDefault,
1000
+ id: item.id
1001
+ });
1002
+ }
1003
+ function inspectHeuristicItem(candidate) {
1004
+ const app = appNameFromPath(candidate.path);
1005
+ const kind = heuristicKind(candidate.path);
1006
+ const shareability = heuristicShareability(candidate.path, kind);
1007
+ return inspectPath({
1008
+ path: candidate.path,
1009
+ domain: heuristicDomain(candidate.path),
1010
+ app,
1011
+ kind,
1012
+ format: detectFormat(candidate.path),
1013
+ shareability,
1014
+ reason: candidate.reason,
1015
+ mirrors: [],
1016
+ privateByDefault: shareability === "private"
1017
+ });
1018
+ }
1019
+ function inspectPath(input) {
1020
+ const exists = existsSync3(input.path);
1021
+ const lst = exists ? lstatSync(input.path) : undefined;
1022
+ const stat = exists ? statSync(input.path) : undefined;
1023
+ const isDirectory = !!stat?.isDirectory();
1024
+ const isSymlink = !!lst?.isSymbolicLink();
1025
+ const secretFindings = exists && !isDirectory && isSafeTextPath(input.path) ? detectSecrets(readFileSync3(input.path, "utf-8")) : [];
1026
+ const mirrors = input.mirrors.map((mirror) => inspectMirror(input.path, expandHome(mirror)));
1027
+ const recommendation = recommend(input.kind, input.shareability, secretFindings.length, mirrors);
1028
+ const previewInfo = preview(input.path, exists, isDirectory, input.shareability, input.privateByDefault, input.kind);
1029
+ return {
1030
+ id: input.id ?? stableId(input.path),
1031
+ domain: input.domain,
1032
+ app: input.app,
1033
+ path: input.path,
1034
+ displayPath: displayPath(input.path),
1035
+ kind: input.kind,
1036
+ format: input.format,
1037
+ shareability: input.shareability,
1038
+ exists,
1039
+ isDirectory,
1040
+ isSymlink,
1041
+ mode: stat ? `0${(stat.mode & 511).toString(8)}` : undefined,
1042
+ owner: exists ? userInfo().username : undefined,
1043
+ size: stat?.size,
1044
+ git: exists ? gitInfo(input.path) : undefined,
1045
+ legacyManagers: exists ? legacyManagerInfo(input.path) : {},
1046
+ mirrors,
1047
+ secretFindings,
1048
+ ...previewInfo,
1049
+ recommendation,
1050
+ reason: input.reason
1051
+ };
1052
+ }
1053
+ function discoverConfigCandidates() {
1054
+ const candidates = [];
1055
+ const configRoot = join3(HOME, ".config");
1056
+ if (!existsSync3(configRoot))
1057
+ return candidates;
1058
+ for (const entry of readdirSync(configRoot, { withFileTypes: true })) {
1059
+ if (entry.name.startsWith("."))
1060
+ continue;
1061
+ const appDir = join3(configRoot, entry.name);
1062
+ candidates.push({ path: appDir, reason: "~/.config app directory" });
1063
+ if (!entry.isDirectory())
1064
+ continue;
1065
+ for (const child of safeReadDir(appDir).slice(0, 80)) {
1066
+ const childPath = join3(appDir, child.name);
1067
+ if (isGeneratedPath(childPath)) {
1068
+ candidates.push({ path: childPath, reason: "generated/app-state candidate" });
1069
+ continue;
1070
+ }
1071
+ if (child.isFile() && looksLikeConfigFile(child.name)) {
1072
+ candidates.push({ path: childPath, reason: "shallow ~/.config config candidate" });
1073
+ }
1074
+ if (child.isDirectory() && ["conf.d", "themes", "snippets", "plugins"].includes(child.name)) {
1075
+ candidates.push({ path: childPath, reason: "shallow ~/.config config directory" });
1076
+ }
1077
+ }
1078
+ }
1079
+ return candidates;
1080
+ }
1081
+ function safeReadDir(path) {
1082
+ try {
1083
+ return readdirSync(path, { withFileTypes: true });
1084
+ } catch {
1085
+ return [];
1086
+ }
1087
+ }
1088
+ function inspectMirror(livePath, mirrorPath) {
1089
+ const exists = existsSync3(mirrorPath);
1090
+ if (!exists || !existsSync3(livePath) || statSync(livePath).isDirectory() || statSync(mirrorPath).isDirectory()) {
1091
+ return { path: mirrorPath, displayPath: displayPath(mirrorPath), exists };
1092
+ }
1093
+ const live = readFileSync3(livePath, "utf-8");
1094
+ const mirror = readFileSync3(mirrorPath, "utf-8");
1095
+ return {
1096
+ path: mirrorPath,
1097
+ displayPath: displayPath(mirrorPath),
1098
+ exists,
1099
+ identical: live === mirror,
1100
+ diff: live === mirror ? undefined : simpleDiff(mirror, live)
1101
+ };
1102
+ }
1103
+ function simpleDiff(before, after) {
1104
+ const beforeLines = before.split(/\r?\n/);
1105
+ const afterLines = after.split(/\r?\n/);
1106
+ const max = Math.max(beforeLines.length, afterLines.length);
1107
+ const output = [];
1108
+ for (let index = 0;index < max; index++) {
1109
+ if (beforeLines[index] === afterLines[index])
1110
+ continue;
1111
+ if (beforeLines[index] !== undefined)
1112
+ output.push(`-${beforeLines[index]}`);
1113
+ if (afterLines[index] !== undefined)
1114
+ output.push(`+${afterLines[index]}`);
1115
+ }
1116
+ return output.slice(0, 200).join(`
1117
+ `);
1118
+ }
1119
+ function preview(path, exists, isDirectory, shareability, privateByDefault, kind) {
1120
+ if (!exists)
1121
+ return { previewSuppressedReason: "missing" };
1122
+ if (isDirectory)
1123
+ return { previewSuppressedReason: "directory" };
1124
+ if (shareability === "private" || privateByDefault || kind === "private")
1125
+ return { previewSuppressedReason: "private/auth metadata-only" };
1126
+ if (!isSafeTextPath(path))
1127
+ return { previewSuppressedReason: "binary or unsupported file type" };
1128
+ const text = readFileSync3(path, "utf-8");
1129
+ return { preview: redactText(text).slice(0, 20000) };
1130
+ }
1131
+ function recommend(kind, shareability, secretCount, mirrors) {
1132
+ if (secretCount > 0)
1133
+ return "rotate-secret";
1134
+ if (kind === "generated")
1135
+ return "ignore-generated";
1136
+ if (shareability === "private")
1137
+ return "mark-private";
1138
+ if (mirrors.some((mirror) => mirror.exists && mirror.identical === false))
1139
+ return "resolve-drift";
1140
+ if (shareability === "machine-specific")
1141
+ return "review-machine-specific";
1142
+ if (kind === "config" || kind === "directory")
1143
+ return "adopt-candidate";
1144
+ return "none";
1145
+ }
1146
+ function gitInfo(path) {
1147
+ const cwd = statSync(path).isDirectory() ? path : dirname3(path);
1148
+ const root = git(["rev-parse", "--show-toplevel"], cwd);
1149
+ if (!root)
1150
+ return;
1151
+ const rel = relative(root, path);
1152
+ const ignored = !!git(["check-ignore", "-q", rel], root, true);
1153
+ const tracked = !!git(["ls-files", "--error-unmatch", rel], root, true);
1154
+ const modified = tracked && !!git(["status", "--porcelain", "--", rel], root);
1155
+ return { root, tracked, modified, ignored };
1156
+ }
1157
+ function legacyManagerInfo(path) {
1158
+ if (!yadmCache)
1159
+ return {};
1160
+ return { yadm: yadmInfo(path) };
1161
+ }
1162
+ function yadmInfo(path) {
1163
+ const rel = displayPath(path).replace(/^~\//, "");
1164
+ if (yadmCache?.modified.has(rel))
1165
+ return "modified";
1166
+ if (yadmCache?.tracked.has(rel))
1167
+ return "tracked";
1168
+ return "unknown";
1169
+ }
1170
+ function loadYadmCache() {
1171
+ const tracked = new Set((gitLike("yadm", ["ls-files"]) ?? "").split(`
1172
+ `).filter(Boolean));
1173
+ const modified = new Set((gitLike("yadm", ["status", "--porcelain"]) ?? "").split(`
1174
+ `).map((line) => line.slice(3).trim()).filter(Boolean));
1175
+ return { tracked, modified };
1176
+ }
1177
+ function git(args, cwd, allowEmpty = false) {
1178
+ try {
1179
+ const out = execFileSync3("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1180
+ return out || (allowEmpty ? "ok" : undefined);
1181
+ } catch {
1182
+ return;
1183
+ }
1184
+ }
1185
+ function gitLike(command, args) {
1186
+ try {
1187
+ return execFileSync3(command, args, { cwd: HOME, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1188
+ } catch {
1189
+ return;
1190
+ }
1191
+ }
1192
+ function expandHome(path) {
1193
+ return resolve(path.replace(/^~(?=\/|$)/, HOME));
1194
+ }
1195
+ function displayPath(path) {
1196
+ return path.startsWith(HOME) ? `~${path.slice(HOME.length)}` : path;
1197
+ }
1198
+ function stableId(path) {
1199
+ return displayPath(path).replace(/[^A-Za-z0-9_.-]+/g, ":");
1200
+ }
1201
+ function appNameFromPath(path) {
1202
+ const rel = relative(join3(HOME, ".config"), path);
1203
+ if (!rel.startsWith(".."))
1204
+ return rel.split(/[\\/]/)[0] || "Unknown";
1205
+ return "Unknown";
1206
+ }
1207
+ function heuristicDomain(path) {
1208
+ if (path.includes("/.config/"))
1209
+ return "~/.config";
1210
+ if (path.includes("/.ssh") || path.includes("/.aws"))
1211
+ return "Private/Auth";
1212
+ return "Unknown";
1213
+ }
1214
+ function heuristicKind(path) {
1215
+ if (isGeneratedPath(path))
1216
+ return "generated";
1217
+ if (PRIVATE_NAMES.some((pattern) => pattern.test(path)))
1218
+ return "private";
1219
+ if (existsSync3(path) && statSync(path).isDirectory())
1220
+ return "directory";
1221
+ if (looksLikeConfigFile(path))
1222
+ return "config";
1223
+ return "unknown";
1224
+ }
1225
+ function heuristicShareability(path, kind) {
1226
+ if (kind === "private")
1227
+ return "private";
1228
+ if (kind === "generated")
1229
+ return "machine-specific";
1230
+ if (path.includes("/credentials") || path.includes("/.ssh") || path.includes("/.aws"))
1231
+ return "private";
1232
+ return "machine-specific";
1233
+ }
1234
+ function detectFormat(path) {
1235
+ if (existsSync3(path) && statSync(path).isDirectory())
1236
+ return "directory";
1237
+ if (path.endsWith(".jsonc"))
1238
+ return "jsonc";
1239
+ if (path.endsWith(".json"))
1240
+ return "json";
1241
+ if (path.endsWith(".toml"))
1242
+ return "toml";
1243
+ if (path.endsWith(".zsh"))
1244
+ return "shell";
1245
+ if (path.endsWith(".md"))
1246
+ return "markdown";
1247
+ return "text";
1248
+ }
1249
+ function isGeneratedPath(path) {
1250
+ return GENERATED_NAMES.some((pattern) => pattern.test(path));
1251
+ }
1252
+ function looksLikeConfigFile(name) {
1253
+ return /(^config\.|settings\.|rc$|\.rc$|\.zshrc$|\.zshenv$|\.jsonc?$|\.toml$|\.ya?ml$|\.zsh$|\.conf$|\.ini$)/i.test(name);
1254
+ }
1255
+ function isSafeTextPath(path) {
1256
+ const name = path.split("/").pop() ?? "";
1257
+ if (isGeneratedPath(path))
1258
+ return false;
1259
+ if (name.includes("lock") || name.endsWith(".mdb") || name.endsWith(".sqlite") || name.endsWith(".db"))
1260
+ return false;
1261
+ const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")) : "";
1262
+ return TEXT_EXTENSIONS.has(ext) || looksLikeConfigFile(name);
1263
+ }
1264
+
1265
+ // src/ui.tsx
1266
+ import { Box, Text, render } from "ink";
1267
+ import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
1268
+ function renderInk(element) {
1269
+ const app = render(/* @__PURE__ */ jsxDEV(Fragment, {
1270
+ children: element
1271
+ }, undefined, false, undefined, this));
1272
+ return app.waitUntilExit();
1273
+ }
1274
+ function ScanSummary({ inventory, path }) {
1275
+ return /* @__PURE__ */ jsxDEV(Box, {
1276
+ flexDirection: "column",
1277
+ gap: 1,
1278
+ children: [
1279
+ /* @__PURE__ */ jsxDEV(Header, {
1280
+ title: "Scan complete",
1281
+ subtitle: path
1282
+ }, undefined, false, undefined, this),
1283
+ /* @__PURE__ */ jsxDEV(Box, {
1284
+ gap: 2,
1285
+ children: [
1286
+ /* @__PURE__ */ jsxDEV(Stat, {
1287
+ label: "total",
1288
+ value: inventory.summary.total,
1289
+ color: "cyan"
1290
+ }, undefined, false, undefined, this),
1291
+ /* @__PURE__ */ jsxDEV(Stat, {
1292
+ label: "existing",
1293
+ value: inventory.summary.existing,
1294
+ color: "green"
1295
+ }, undefined, false, undefined, this),
1296
+ /* @__PURE__ */ jsxDEV(Stat, {
1297
+ label: "private",
1298
+ value: inventory.summary.private,
1299
+ color: "red"
1300
+ }, undefined, false, undefined, this),
1301
+ /* @__PURE__ */ jsxDEV(Stat, {
1302
+ label: "generated",
1303
+ value: inventory.summary.generated,
1304
+ color: "magenta"
1305
+ }, undefined, false, undefined, this),
1306
+ /* @__PURE__ */ jsxDEV(Stat, {
1307
+ label: "secrets",
1308
+ value: inventory.summary.secrets,
1309
+ color: "red"
1310
+ }, undefined, false, undefined, this),
1311
+ /* @__PURE__ */ jsxDEV(Stat, {
1312
+ label: "drift",
1313
+ value: inventory.summary.drift,
1314
+ color: "blue"
1315
+ }, undefined, false, undefined, this)
1316
+ ]
1317
+ }, undefined, true, undefined, this)
1318
+ ]
1319
+ }, undefined, true, undefined, this);
1320
+ }
1321
+ function ItemList({ items, title }) {
1322
+ return /* @__PURE__ */ jsxDEV(Box, {
1323
+ flexDirection: "column",
1324
+ children: [
1325
+ /* @__PURE__ */ jsxDEV(Header, {
1326
+ title,
1327
+ subtitle: `${items.length} item${items.length === 1 ? "" : "s"}`
1328
+ }, undefined, false, undefined, this),
1329
+ /* @__PURE__ */ jsxDEV(Box, {
1330
+ flexDirection: "column",
1331
+ marginTop: 1,
1332
+ children: items.map((item) => /* @__PURE__ */ jsxDEV(Box, {
1333
+ gap: 1,
1334
+ children: [
1335
+ /* @__PURE__ */ jsxDEV(Text, {
1336
+ color: item.exists ? "white" : "gray",
1337
+ children: item.exists ? "\u25CF" : "\u25CB"
1338
+ }, undefined, false, undefined, this),
1339
+ /* @__PURE__ */ jsxDEV(Text, {
1340
+ color: kindColor(item),
1341
+ children: item.domain
1342
+ }, undefined, false, undefined, this),
1343
+ /* @__PURE__ */ jsxDEV(Text, {
1344
+ color: "gray",
1345
+ children: "/"
1346
+ }, undefined, false, undefined, this),
1347
+ /* @__PURE__ */ jsxDEV(Text, {
1348
+ color: "cyan",
1349
+ children: item.app
1350
+ }, undefined, false, undefined, this),
1351
+ /* @__PURE__ */ jsxDEV(Text, {
1352
+ children: item.displayPath
1353
+ }, undefined, false, undefined, this),
1354
+ /* @__PURE__ */ jsxDEV(Badge, {
1355
+ label: item.shareability,
1356
+ color: shareabilityColor(item.shareability)
1357
+ }, undefined, false, undefined, this),
1358
+ item.secretFindings.length > 0 ? /* @__PURE__ */ jsxDEV(Badge, {
1359
+ label: "secret",
1360
+ color: "red"
1361
+ }, undefined, false, undefined, this) : null,
1362
+ item.mirrors.some((mirror) => mirror.exists && mirror.identical === false) ? /* @__PURE__ */ jsxDEV(Badge, {
1363
+ label: "drift",
1364
+ color: "blue"
1365
+ }, undefined, false, undefined, this) : null
1366
+ ]
1367
+ }, item.id, true, undefined, this))
1368
+ }, undefined, false, undefined, this)
1369
+ ]
1370
+ }, undefined, true, undefined, this);
1371
+ }
1372
+ function ItemDetail({ item }) {
1373
+ return /* @__PURE__ */ jsxDEV(Box, {
1374
+ flexDirection: "column",
1375
+ gap: 1,
1376
+ children: [
1377
+ /* @__PURE__ */ jsxDEV(Header, {
1378
+ title: item.app,
1379
+ subtitle: item.displayPath
1380
+ }, undefined, false, undefined, this),
1381
+ /* @__PURE__ */ jsxDEV(Box, {
1382
+ gap: 1,
1383
+ children: [
1384
+ /* @__PURE__ */ jsxDEV(Badge, {
1385
+ label: item.domain,
1386
+ color: "cyan"
1387
+ }, undefined, false, undefined, this),
1388
+ /* @__PURE__ */ jsxDEV(Badge, {
1389
+ label: item.kind,
1390
+ color: kindColor(item)
1391
+ }, undefined, false, undefined, this),
1392
+ /* @__PURE__ */ jsxDEV(Badge, {
1393
+ label: item.shareability,
1394
+ color: shareabilityColor(item.shareability)
1395
+ }, undefined, false, undefined, this),
1396
+ /* @__PURE__ */ jsxDEV(Badge, {
1397
+ label: item.recommendation,
1398
+ color: "yellow"
1399
+ }, undefined, false, undefined, this)
1400
+ ]
1401
+ }, undefined, true, undefined, this),
1402
+ /* @__PURE__ */ jsxDEV(KeyValues, {
1403
+ rows: [
1404
+ ["reason", item.reason],
1405
+ ["exists", String(item.exists)],
1406
+ ["mode", item.mode ?? "\u2014"],
1407
+ ["symlink", String(item.isSymlink)],
1408
+ ["git", gitText(item)],
1409
+ ["legacy", legacyText(item)]
1410
+ ]
1411
+ }, undefined, false, undefined, this),
1412
+ item.secretFindings.length > 0 ? /* @__PURE__ */ jsxDEV(Box, {
1413
+ flexDirection: "column",
1414
+ children: [
1415
+ /* @__PURE__ */ jsxDEV(Text, {
1416
+ color: "red",
1417
+ bold: true,
1418
+ children: "Secret warnings"
1419
+ }, undefined, false, undefined, this),
1420
+ item.secretFindings.map((finding, index) => /* @__PURE__ */ jsxDEV(Text, {
1421
+ color: "red",
1422
+ children: [
1423
+ " ",
1424
+ finding.type,
1425
+ finding.line ? ` line ${finding.line}` : "",
1426
+ ": ",
1427
+ finding.preview
1428
+ ]
1429
+ }, `${finding.type}-${index}`, true, undefined, this))
1430
+ ]
1431
+ }, undefined, true, undefined, this) : null,
1432
+ item.mirrors.length > 0 ? /* @__PURE__ */ jsxDEV(Box, {
1433
+ flexDirection: "column",
1434
+ children: [
1435
+ /* @__PURE__ */ jsxDEV(Text, {
1436
+ color: "blue",
1437
+ bold: true,
1438
+ children: "Mirrors"
1439
+ }, undefined, false, undefined, this),
1440
+ item.mirrors.map((mirror) => /* @__PURE__ */ jsxDEV(Text, {
1441
+ children: [
1442
+ " ",
1443
+ mirror.displayPath,
1444
+ " \u2014 ",
1445
+ mirror.exists ? mirror.identical === false ? "differs" : "identical" : "missing"
1446
+ ]
1447
+ }, mirror.path, true, undefined, this))
1448
+ ]
1449
+ }, undefined, true, undefined, this) : null,
1450
+ /* @__PURE__ */ jsxDEV(Box, {
1451
+ flexDirection: "column",
1452
+ children: [
1453
+ /* @__PURE__ */ jsxDEV(Text, {
1454
+ bold: true,
1455
+ children: item.preview ? "Safe preview" : "Preview"
1456
+ }, undefined, false, undefined, this),
1457
+ /* @__PURE__ */ jsxDEV(Text, {
1458
+ color: item.preview ? "white" : "gray",
1459
+ children: item.preview ? item.preview.slice(0, 3000) : `Suppressed: ${item.previewSuppressedReason ?? "not available"}`
1460
+ }, undefined, false, undefined, this)
1461
+ ]
1462
+ }, undefined, true, undefined, this)
1463
+ ]
1464
+ }, undefined, true, undefined, this);
1465
+ }
1466
+ function DecisionsView({ decisions }) {
1467
+ return /* @__PURE__ */ jsxDEV(Box, {
1468
+ flexDirection: "column",
1469
+ children: [
1470
+ /* @__PURE__ */ jsxDEV(Header, {
1471
+ title: "Decisions",
1472
+ subtitle: `${decisions.length} saved`
1473
+ }, undefined, false, undefined, this),
1474
+ /* @__PURE__ */ jsxDEV(Box, {
1475
+ flexDirection: "column",
1476
+ marginTop: 1,
1477
+ children: decisions.length === 0 ? /* @__PURE__ */ jsxDEV(Text, {
1478
+ color: "gray",
1479
+ children: "No decisions recorded yet."
1480
+ }, undefined, false, undefined, this) : decisions.map((decision) => /* @__PURE__ */ jsxDEV(Box, {
1481
+ gap: 1,
1482
+ children: [
1483
+ /* @__PURE__ */ jsxDEV(Badge, {
1484
+ label: decision.status,
1485
+ color: "cyan"
1486
+ }, undefined, false, undefined, this),
1487
+ /* @__PURE__ */ jsxDEV(Text, {
1488
+ children: decision.itemId
1489
+ }, undefined, false, undefined, this),
1490
+ /* @__PURE__ */ jsxDEV(Text, {
1491
+ color: "gray",
1492
+ children: decision.updatedAt
1493
+ }, undefined, false, undefined, this)
1494
+ ]
1495
+ }, `${decision.itemId}-${decision.updatedAt}`, true, undefined, this))
1496
+ }, undefined, false, undefined, this)
1497
+ ]
1498
+ }, undefined, true, undefined, this);
1499
+ }
1500
+ function LoginPrompt({ verificationUri, userCode, expiresIn }) {
1501
+ return /* @__PURE__ */ jsxDEV(Box, {
1502
+ flexDirection: "column",
1503
+ gap: 1,
1504
+ children: [
1505
+ /* @__PURE__ */ jsxDEV(Header, {
1506
+ title: "Login with GitHub",
1507
+ subtitle: `code expires in ${Math.round(expiresIn / 60)} minutes`
1508
+ }, undefined, false, undefined, this),
1509
+ /* @__PURE__ */ jsxDEV(Box, {
1510
+ flexDirection: "column",
1511
+ children: [
1512
+ /* @__PURE__ */ jsxDEV(Text, {
1513
+ children: "Open:"
1514
+ }, undefined, false, undefined, this),
1515
+ /* @__PURE__ */ jsxDEV(Text, {
1516
+ color: "blue",
1517
+ underline: true,
1518
+ children: verificationUri
1519
+ }, undefined, false, undefined, this)
1520
+ ]
1521
+ }, undefined, true, undefined, this),
1522
+ /* @__PURE__ */ jsxDEV(Box, {
1523
+ gap: 1,
1524
+ children: [
1525
+ /* @__PURE__ */ jsxDEV(Text, {
1526
+ children: "Enter code:"
1527
+ }, undefined, false, undefined, this),
1528
+ /* @__PURE__ */ jsxDEV(Text, {
1529
+ color: "green",
1530
+ bold: true,
1531
+ children: userCode
1532
+ }, undefined, false, undefined, this)
1533
+ ]
1534
+ }, undefined, true, undefined, this)
1535
+ ]
1536
+ }, undefined, true, undefined, this);
1537
+ }
1538
+ function LoginSuccess({ login, apiUrl }) {
1539
+ return /* @__PURE__ */ jsxDEV(Box, {
1540
+ flexDirection: "column",
1541
+ children: [
1542
+ /* @__PURE__ */ jsxDEV(Header, {
1543
+ title: "Logged in",
1544
+ subtitle: apiUrl
1545
+ }, undefined, false, undefined, this),
1546
+ /* @__PURE__ */ jsxDEV(Text, {
1547
+ color: "green",
1548
+ children: [
1549
+ "\u2713 ",
1550
+ login
1551
+ ]
1552
+ }, undefined, true, undefined, this)
1553
+ ]
1554
+ }, undefined, true, undefined, this);
1555
+ }
1556
+ function WhoamiView({ login, apiUrl }) {
1557
+ if (!login)
1558
+ return /* @__PURE__ */ jsxDEV(Text, {
1559
+ color: "yellow",
1560
+ children: "Not logged in. Run `atl login`."
1561
+ }, undefined, false, undefined, this);
1562
+ return /* @__PURE__ */ jsxDEV(Box, {
1563
+ flexDirection: "column",
1564
+ children: [
1565
+ /* @__PURE__ */ jsxDEV(Header, {
1566
+ title: "Current account",
1567
+ subtitle: apiUrl
1568
+ }, undefined, false, undefined, this),
1569
+ /* @__PURE__ */ jsxDEV(Text, {
1570
+ color: "green",
1571
+ children: login
1572
+ }, undefined, false, undefined, this)
1573
+ ]
1574
+ }, undefined, true, undefined, this);
1575
+ }
1576
+ function LogoutView() {
1577
+ return /* @__PURE__ */ jsxDEV(Text, {
1578
+ color: "green",
1579
+ children: "\u2713 Logged out"
1580
+ }, undefined, false, undefined, this);
1581
+ }
1582
+ function ProfileListView({ profiles, activeProfile }) {
1583
+ return /* @__PURE__ */ jsxDEV(Box, {
1584
+ flexDirection: "column",
1585
+ children: [
1586
+ /* @__PURE__ */ jsxDEV(Header, {
1587
+ title: "Profiles",
1588
+ subtitle: `active: ${activeProfile}`
1589
+ }, undefined, false, undefined, this),
1590
+ /* @__PURE__ */ jsxDEV(Box, {
1591
+ flexDirection: "column",
1592
+ marginTop: 1,
1593
+ children: profiles.map((profile) => /* @__PURE__ */ jsxDEV(Box, {
1594
+ gap: 1,
1595
+ children: [
1596
+ /* @__PURE__ */ jsxDEV(Text, {
1597
+ color: profile.name === activeProfile ? "green" : "gray",
1598
+ children: profile.name === activeProfile ? "\u25CF" : "\u25CB"
1599
+ }, undefined, false, undefined, this),
1600
+ /* @__PURE__ */ jsxDEV(Text, {
1601
+ bold: profile.name === activeProfile,
1602
+ children: profile.name
1603
+ }, undefined, false, undefined, this),
1604
+ /* @__PURE__ */ jsxDEV(Text, {
1605
+ color: "gray",
1606
+ children: profile.createdAt
1607
+ }, undefined, false, undefined, this)
1608
+ ]
1609
+ }, profile.id, true, undefined, this))
1610
+ }, undefined, false, undefined, this)
1611
+ ]
1612
+ }, undefined, true, undefined, this);
1613
+ }
1614
+ function ProfileSavedView({ title, name }) {
1615
+ return /* @__PURE__ */ jsxDEV(Box, {
1616
+ flexDirection: "column",
1617
+ children: [
1618
+ /* @__PURE__ */ jsxDEV(Header, {
1619
+ title
1620
+ }, undefined, false, undefined, this),
1621
+ /* @__PURE__ */ jsxDEV(Text, {
1622
+ color: "green",
1623
+ children: [
1624
+ "\u2713 ",
1625
+ name
1626
+ ]
1627
+ }, undefined, true, undefined, this)
1628
+ ]
1629
+ }, undefined, true, undefined, this);
1630
+ }
1631
+ function SavedConfigListView({ configs, profileName }) {
1632
+ return /* @__PURE__ */ jsxDEV(Box, {
1633
+ flexDirection: "column",
1634
+ children: [
1635
+ /* @__PURE__ */ jsxDEV(Header, {
1636
+ title: "Saved configs",
1637
+ subtitle: profileName
1638
+ }, undefined, false, undefined, this),
1639
+ configs.length === 0 ? /* @__PURE__ */ jsxDEV(Text, {
1640
+ color: "gray",
1641
+ children: "No saved configs yet."
1642
+ }, undefined, false, undefined, this) : configs.map((config) => /* @__PURE__ */ jsxDEV(Box, {
1643
+ flexDirection: "column",
1644
+ marginTop: 1,
1645
+ children: [
1646
+ /* @__PURE__ */ jsxDEV(Text, {
1647
+ bold: true,
1648
+ children: config.stableId
1649
+ }, undefined, false, undefined, this),
1650
+ /* @__PURE__ */ jsxDEV(Text, {
1651
+ color: "gray",
1652
+ children: config.pathHint ?? "no path hint"
1653
+ }, undefined, false, undefined, this),
1654
+ config.latestVersion ? /* @__PURE__ */ jsxDEV(Text, {
1655
+ color: "green",
1656
+ children: [
1657
+ config.latestVersion.contentSha256.slice(0, 12),
1658
+ " \xB7 ",
1659
+ config.latestVersion.sizeBytes,
1660
+ " bytes \xB7 ",
1661
+ config.latestVersion.createdAt
1662
+ ]
1663
+ }, undefined, true, undefined, this) : null
1664
+ ]
1665
+ }, config.id, true, undefined, this))
1666
+ ]
1667
+ }, undefined, true, undefined, this);
1668
+ }
1669
+ function SavedConfigDetailView({ config, profileName }) {
1670
+ return /* @__PURE__ */ jsxDEV(Box, {
1671
+ flexDirection: "column",
1672
+ children: [
1673
+ /* @__PURE__ */ jsxDEV(Header, {
1674
+ title: config.stableId,
1675
+ subtitle: profileName
1676
+ }, undefined, false, undefined, this),
1677
+ /* @__PURE__ */ jsxDEV(Text, {
1678
+ color: "gray",
1679
+ children: config.pathHint ?? "no path hint"
1680
+ }, undefined, false, undefined, this),
1681
+ /* @__PURE__ */ jsxDEV(Box, {
1682
+ flexDirection: "column",
1683
+ marginTop: 1,
1684
+ children: (config.versions ?? []).map((version) => /* @__PURE__ */ jsxDEV(Text, {
1685
+ children: [
1686
+ version.contentSha256.slice(0, 12),
1687
+ " \xB7 ",
1688
+ version.sizeBytes,
1689
+ " bytes \xB7 ",
1690
+ version.createdAt
1691
+ ]
1692
+ }, version.id, true, undefined, this))
1693
+ }, undefined, false, undefined, this)
1694
+ ]
1695
+ }, undefined, true, undefined, this);
1696
+ }
1697
+ function ConfigSavedView({ stableId: stableId2, profileName, versionHash, sizeBytes, reused }) {
1698
+ return /* @__PURE__ */ jsxDEV(Box, {
1699
+ flexDirection: "column",
1700
+ children: [
1701
+ /* @__PURE__ */ jsxDEV(Header, {
1702
+ title: reused ? "Config already saved" : "Config saved",
1703
+ subtitle: profileName
1704
+ }, undefined, false, undefined, this),
1705
+ /* @__PURE__ */ jsxDEV(Text, {
1706
+ color: "green",
1707
+ children: [
1708
+ "\u2713 ",
1709
+ stableId2
1710
+ ]
1711
+ }, undefined, true, undefined, this),
1712
+ /* @__PURE__ */ jsxDEV(Text, {
1713
+ color: "gray",
1714
+ children: [
1715
+ "Version ",
1716
+ versionHash.slice(0, 12),
1717
+ " \xB7 ",
1718
+ sizeBytes,
1719
+ " bytes \xB7 encrypted"
1720
+ ]
1721
+ }, undefined, true, undefined, this)
1722
+ ]
1723
+ }, undefined, true, undefined, this);
1724
+ }
1725
+ function DecisionSaved({ item, status }) {
1726
+ return /* @__PURE__ */ jsxDEV(Box, {
1727
+ flexDirection: "column",
1728
+ children: [
1729
+ /* @__PURE__ */ jsxDEV(Header, {
1730
+ title: "Decision saved",
1731
+ subtitle: item.displayPath
1732
+ }, undefined, false, undefined, this),
1733
+ /* @__PURE__ */ jsxDEV(Box, {
1734
+ gap: 1,
1735
+ children: [
1736
+ /* @__PURE__ */ jsxDEV(Text, {
1737
+ children: "Status:"
1738
+ }, undefined, false, undefined, this),
1739
+ /* @__PURE__ */ jsxDEV(Badge, {
1740
+ label: status,
1741
+ color: "green"
1742
+ }, undefined, false, undefined, this)
1743
+ ]
1744
+ }, undefined, true, undefined, this)
1745
+ ]
1746
+ }, undefined, true, undefined, this);
1747
+ }
1748
+ function DoctorView({ checks }) {
1749
+ const failing = checks.filter((check) => check.status === "fail").length;
1750
+ const warnings = checks.filter((check) => check.status === "warn").length;
1751
+ return /* @__PURE__ */ jsxDEV(Box, {
1752
+ flexDirection: "column",
1753
+ gap: 1,
1754
+ children: [
1755
+ /* @__PURE__ */ jsxDEV(Header, {
1756
+ title: "Doctor",
1757
+ subtitle: failing ? `${failing} failing` : warnings ? `${warnings} warning${warnings === 1 ? "" : "s"}` : "all checks passed"
1758
+ }, undefined, false, undefined, this),
1759
+ /* @__PURE__ */ jsxDEV(Box, {
1760
+ flexDirection: "column",
1761
+ children: checks.map((check) => /* @__PURE__ */ jsxDEV(Box, {
1762
+ gap: 1,
1763
+ children: [
1764
+ /* @__PURE__ */ jsxDEV(Text, {
1765
+ color: doctorColor(check.status),
1766
+ children: doctorIcon(check.status)
1767
+ }, undefined, false, undefined, this),
1768
+ /* @__PURE__ */ jsxDEV(Text, {
1769
+ bold: true,
1770
+ children: check.label
1771
+ }, undefined, false, undefined, this),
1772
+ /* @__PURE__ */ jsxDEV(Text, {
1773
+ color: "gray",
1774
+ children: check.detail
1775
+ }, undefined, false, undefined, this)
1776
+ ]
1777
+ }, check.label, true, undefined, this))
1778
+ }, undefined, false, undefined, this)
1779
+ ]
1780
+ }, undefined, true, undefined, this);
1781
+ }
1782
+ function NotFound({ query }) {
1783
+ return /* @__PURE__ */ jsxDEV(Text, {
1784
+ color: "red",
1785
+ children: [
1786
+ "No inventory item matched: ",
1787
+ query
1788
+ ]
1789
+ }, undefined, true, undefined, this);
1790
+ }
1791
+ function Header({ title, subtitle }) {
1792
+ return /* @__PURE__ */ jsxDEV(Box, {
1793
+ flexDirection: "column",
1794
+ children: [
1795
+ /* @__PURE__ */ jsxDEV(Text, {
1796
+ bold: true,
1797
+ color: "cyan",
1798
+ children: "\u25C6 Atelier"
1799
+ }, undefined, false, undefined, this),
1800
+ /* @__PURE__ */ jsxDEV(Box, {
1801
+ gap: 1,
1802
+ children: [
1803
+ /* @__PURE__ */ jsxDEV(Text, {
1804
+ bold: true,
1805
+ children: title
1806
+ }, undefined, false, undefined, this),
1807
+ subtitle ? /* @__PURE__ */ jsxDEV(Text, {
1808
+ color: "gray",
1809
+ children: subtitle
1810
+ }, undefined, false, undefined, this) : null
1811
+ ]
1812
+ }, undefined, true, undefined, this)
1813
+ ]
1814
+ }, undefined, true, undefined, this);
1815
+ }
1816
+ function Stat({ label, value, color }) {
1817
+ return /* @__PURE__ */ jsxDEV(Box, {
1818
+ flexDirection: "column",
1819
+ borderStyle: "round",
1820
+ borderColor: color,
1821
+ paddingX: 1,
1822
+ children: [
1823
+ /* @__PURE__ */ jsxDEV(Text, {
1824
+ color,
1825
+ bold: true,
1826
+ children: value
1827
+ }, undefined, false, undefined, this),
1828
+ /* @__PURE__ */ jsxDEV(Text, {
1829
+ color: "gray",
1830
+ children: label
1831
+ }, undefined, false, undefined, this)
1832
+ ]
1833
+ }, undefined, true, undefined, this);
1834
+ }
1835
+ function Badge({ label, color }) {
1836
+ return /* @__PURE__ */ jsxDEV(Text, {
1837
+ color,
1838
+ children: [
1839
+ "[",
1840
+ label,
1841
+ "]"
1842
+ ]
1843
+ }, undefined, true, undefined, this);
1844
+ }
1845
+ function KeyValues({ rows }) {
1846
+ return /* @__PURE__ */ jsxDEV(Box, {
1847
+ flexDirection: "column",
1848
+ children: rows.map(([key, value]) => /* @__PURE__ */ jsxDEV(Box, {
1849
+ gap: 1,
1850
+ children: [
1851
+ /* @__PURE__ */ jsxDEV(Text, {
1852
+ color: "gray",
1853
+ children: key.padEnd(10)
1854
+ }, undefined, false, undefined, this),
1855
+ /* @__PURE__ */ jsxDEV(Text, {
1856
+ children: value
1857
+ }, undefined, false, undefined, this)
1858
+ ]
1859
+ }, key, true, undefined, this))
1860
+ }, undefined, false, undefined, this);
1861
+ }
1862
+ function kindColor(item) {
1863
+ const kind = typeof item === "string" ? item : item.kind;
1864
+ if (kind === "private")
1865
+ return "red";
1866
+ if (kind === "generated")
1867
+ return "magenta";
1868
+ if (kind === "config")
1869
+ return "green";
1870
+ return "yellow";
1871
+ }
1872
+ function shareabilityColor(shareability) {
1873
+ if (shareability === "private")
1874
+ return "red";
1875
+ if (shareability === "shareable")
1876
+ return "green";
1877
+ return "yellow";
1878
+ }
1879
+ function doctorIcon(status) {
1880
+ if (status === "pass")
1881
+ return "\u2713";
1882
+ if (status === "warn")
1883
+ return "!";
1884
+ return "\u2717";
1885
+ }
1886
+ function doctorColor(status) {
1887
+ if (status === "pass")
1888
+ return "green";
1889
+ if (status === "warn")
1890
+ return "yellow";
1891
+ return "red";
1892
+ }
1893
+ function legacyText(item) {
1894
+ const entries = Object.entries(item.legacyManagers ?? {});
1895
+ if (entries.length === 0)
1896
+ return "not scanned";
1897
+ return entries.map(([manager, status]) => `${manager}: ${status}`).join(", ");
1898
+ }
1899
+ function gitText(item) {
1900
+ if (!item.git)
1901
+ return "not in repo";
1902
+ return `${item.git.tracked ? "tracked" : "untracked"}${item.git.modified ? ", modified" : ""} @ ${item.git.root}`;
1903
+ }
1904
+
1905
+ // src/index.tsx
1906
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
1907
+ var repoRoot = resolve2(import.meta.dir, "../../..");
1908
+ var stateDir = getStateDir();
1909
+ var inventoryPath = join4(stateDir, "inventory.json");
1910
+ var decisionsPath = join4(stateDir, "decisions.json");
1911
+ var decisionStatuses = new Set(["undecided", "candidate", "shareable", "machine-specific", "private", "ignored"]);
1912
+ var pipedInputLines;
1913
+ var program = new Command;
1914
+ program.name("atl").description("Atelier config inventory cockpit").version("0.1.0");
1915
+ program.command("login").description("Login to Atelier with GitHub device auth").option("--api-url <url>", "Atelier API URL", getConfiguredApiUrl()).action(async (options) => {
1916
+ const result = await loginWithGitHub({
1917
+ apiUrl: options.apiUrl,
1918
+ onPrompt: async (prompt) => {
1919
+ await renderInk(/* @__PURE__ */ jsxDEV2(LoginPrompt, {
1920
+ verificationUri: prompt.verificationUri,
1921
+ userCode: prompt.userCode,
1922
+ expiresIn: prompt.expiresIn
1923
+ }, undefined, false, undefined, this));
1924
+ }
1925
+ });
1926
+ await renderInk(/* @__PURE__ */ jsxDEV2(LoginSuccess, {
1927
+ login: result.user.login,
1928
+ apiUrl: result.apiUrl
1929
+ }, undefined, false, undefined, this));
1930
+ });
1931
+ program.command("whoami").description("Show the current Atelier account").action(async () => {
1932
+ const current = await getCurrentUser();
1933
+ await renderInk(/* @__PURE__ */ jsxDEV2(WhoamiView, {
1934
+ login: current?.user.login,
1935
+ apiUrl: current?.apiUrl
1936
+ }, undefined, false, undefined, this));
1937
+ if (!current)
1938
+ process.exitCode = 1;
1939
+ });
1940
+ program.command("logout").description("Remove the local Atelier session").action(async () => {
1941
+ logout();
1942
+ await renderInk(/* @__PURE__ */ jsxDEV2(LogoutView, {}, undefined, false, undefined, this));
1943
+ });
1944
+ program.command("api").description("Run the local Atelier API server for development").option("--port <port>", "port to bind", "8787").action(async (options) => {
1945
+ await startApi(Number(options.port));
1946
+ });
1947
+ var profile = program.command("profile").description("Manage account-backed config profiles");
1948
+ profile.command("list").description("List remote profiles").action(async () => {
1949
+ await renderInk(/* @__PURE__ */ jsxDEV2(ProfileListView, {
1950
+ profiles: await listProfiles(),
1951
+ activeProfile: getActiveProfile()
1952
+ }, undefined, false, undefined, this));
1953
+ });
1954
+ profile.command("create").description("Create a remote profile").argument("<name>").action(async (name) => {
1955
+ const profile2 = await createProfile(name);
1956
+ await renderInk(/* @__PURE__ */ jsxDEV2(ProfileSavedView, {
1957
+ title: "Profile created",
1958
+ name: profile2.name
1959
+ }, undefined, false, undefined, this));
1960
+ });
1961
+ profile.command("switch").description("Set the active local profile after verifying it exists remotely").argument("<name>").action(async (name) => {
1962
+ const profile2 = await getProfile(name);
1963
+ if (!profile2)
1964
+ throw new Error(`Profile not found: ${name}`);
1965
+ setActiveProfile(profile2.name);
1966
+ await renderInk(/* @__PURE__ */ jsxDEV2(ProfileSavedView, {
1967
+ title: "Active profile",
1968
+ name: profile2.name
1969
+ }, undefined, false, undefined, this));
1970
+ });
1971
+ profile.command("current").description("Show the active local profile").action(async () => {
1972
+ await renderInk(/* @__PURE__ */ jsxDEV2(ProfileSavedView, {
1973
+ title: "Active profile",
1974
+ name: getActiveProfile()
1975
+ }, undefined, false, undefined, this));
1976
+ });
1977
+ program.command("checkout").description("Alias for `atl profile switch`").argument("<name>").action(async (name) => {
1978
+ const profile2 = await getProfile(name);
1979
+ if (!profile2)
1980
+ throw new Error(`Profile not found: ${name}`);
1981
+ setActiveProfile(profile2.name);
1982
+ await renderInk(/* @__PURE__ */ jsxDEV2(ProfileSavedView, {
1983
+ title: "Active profile",
1984
+ name: profile2.name
1985
+ }, undefined, false, undefined, this));
1986
+ });
1987
+ var vault = program.command("vault").description("Advanced vault controls");
1988
+ vault.command("status").description("Show vault initialization and local unlock state").action(async () => {
1989
+ const state = await getVaultState();
1990
+ console.log(`Vault: ${state.initialized ? "initialized" : "not initialized"}`);
1991
+ console.log(`Local state: ${state.unlocked ? "unlocked" : "locked"}`);
1992
+ console.log(`Active profile: ${state.activeProfile}`);
1993
+ });
1994
+ vault.command("init").description("Initialize the encrypted vault now instead of waiting for first save").action(async () => {
1995
+ const passphrase = await readConfirmedPassphrase("Create an Atelier vault passphrase: ");
1996
+ const result = await initializeVault(passphrase);
1997
+ console.log(`Vault initialized for profile ${result.profileName}`);
1998
+ });
1999
+ vault.command("unlock").description("Unlock this machine with the vault passphrase").action(async () => {
2000
+ const passphrase = await readHiddenLine("Vault passphrase: ");
2001
+ const result = await unlockVault(passphrase);
2002
+ console.log(`Vault unlocked for profile ${result.activeProfile}`);
2003
+ });
2004
+ vault.command("lock").description("Remove local unlocked vault material from this machine").action(async () => {
2005
+ lockVault();
2006
+ console.log("Vault locked");
2007
+ });
2008
+ program.command("save").description("Save a config snapshot to the active encrypted profile").argument("<id-or-path>").action(async (query) => {
2009
+ await saveInventoryItem(query);
2010
+ });
2011
+ program.command("adopt").description("Alias for `atl save`").argument("<id-or-path>").action(async (query) => {
2012
+ await saveInventoryItem(query);
2013
+ });
2014
+ var saved = program.command("saved").description("Inspect encrypted configs saved remotely");
2015
+ saved.command("list").description("List saved configs for the active profile").action(async () => {
2016
+ const profileName = getActiveProfile();
2017
+ await renderInk(/* @__PURE__ */ jsxDEV2(SavedConfigListView, {
2018
+ configs: await listSavedConfigs(profileName),
2019
+ profileName
2020
+ }, undefined, false, undefined, this));
2021
+ });
2022
+ saved.command("show").description("Show saved metadata and version history").argument("<stable-id>").action(async (stableId2) => {
2023
+ const profileName = getActiveProfile();
2024
+ const config = await getSavedConfig(profileName, stableId2);
2025
+ if (!config)
2026
+ throw new Error(`Saved config not found: ${stableId2}`);
2027
+ await renderInk(/* @__PURE__ */ jsxDEV2(SavedConfigDetailView, {
2028
+ config,
2029
+ profileName
2030
+ }, undefined, false, undefined, this));
2031
+ });
2032
+ program.command("scan").description("Scan this machine and write local inventory").option("--path <path...>", "additional manual path(s) to scan").option("--legacy", "include legacy manager signals like yadm").action(async (options) => {
2033
+ const inventory = await scan({ repoRoot, manualPaths: options.path ?? [], includeLegacyManagers: options.legacy });
2034
+ const path = writeInventory(inventory, { path: inventoryPath });
2035
+ await renderInk(/* @__PURE__ */ jsxDEV2(ScanSummary, {
2036
+ inventory,
2037
+ path
2038
+ }, undefined, false, undefined, this));
2039
+ });
2040
+ program.command("ui").description("Open the local TanStack Start inventory UI").option("--scan", "rescan before opening the UI").option("--path <path...>", "additional manual path(s) to scan").option("--legacy", "include legacy manager signals like yadm during scan").option("--port <port>", "port to bind", "4141").action(async (options) => {
2041
+ if (!existsSync4(inventoryPath) || options.scan) {
2042
+ console.log("Atelier: scanning configs\u2026");
2043
+ const inventory = await scan({ repoRoot, manualPaths: options.path ?? [], includeLegacyManagers: options.legacy });
2044
+ writeInventory(inventory, { path: inventoryPath });
2045
+ console.log(`Atelier: scan complete (${inventory.summary.existing}/${inventory.summary.total} existing)`);
2046
+ }
2047
+ console.log("Atelier: starting UI\u2026");
2048
+ await startUi(Number(options.port));
2049
+ });
2050
+ program.command("list").description("List inventory items with polished terminal output").option("--domain <domain>", "filter by domain").option("--app <app>", "filter by app").option("--secrets", "only items with secret warnings").option("--drift", "only items with mirror drift").option("--generated", "only generated/app-state items").option("--private", "only private items").option("--missing", "only missing registry items").option("--json", "print JSON instead of Ink output").action(async (options) => {
2051
+ const inventory = readInventory();
2052
+ const items = filterItems(inventory.items, options);
2053
+ if (options.json)
2054
+ return printJson(items);
2055
+ await renderInk(/* @__PURE__ */ jsxDEV2(ItemList, {
2056
+ items,
2057
+ title: "Inventory"
2058
+ }, undefined, false, undefined, this));
2059
+ });
2060
+ program.command("inspect").description("Inspect one inventory item by id or path").argument("<id-or-path>").option("--json", "print JSON instead of Ink output").action(async (query, options) => {
2061
+ const inventory = readInventory();
2062
+ const item = findItem(inventory, query);
2063
+ if (options.json)
2064
+ return printJson(item ?? null);
2065
+ await renderInk(item ? /* @__PURE__ */ jsxDEV2(ItemDetail, {
2066
+ item
2067
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV2(NotFound, {
2068
+ query
2069
+ }, undefined, false, undefined, this));
2070
+ });
2071
+ program.command("decisions").description("Show saved item decisions").option("--json", "print JSON instead of Ink output").action(async (options) => {
2072
+ const decisions = readDecisions().decisions;
2073
+ if (options.json)
2074
+ return printJson(decisions);
2075
+ await renderInk(/* @__PURE__ */ jsxDEV2(DecisionsView, {
2076
+ decisions
2077
+ }, undefined, false, undefined, this));
2078
+ });
2079
+ program.command("decide").description("Save a decision for one inventory item").argument("<id-or-path>").argument("<status>", `one of: ${Array.from(decisionStatuses).join(", ")}`).action(async (query, status) => {
2080
+ if (!decisionStatuses.has(status)) {
2081
+ program.error(`invalid status '${status}'. Valid: ${Array.from(decisionStatuses).join(", ")}`);
2082
+ }
2083
+ const inventory = readInventory();
2084
+ const item = findItem(inventory, query);
2085
+ if (!item) {
2086
+ await renderInk(/* @__PURE__ */ jsxDEV2(NotFound, {
2087
+ query
2088
+ }, undefined, false, undefined, this));
2089
+ process.exitCode = 1;
2090
+ return;
2091
+ }
2092
+ const decisions = readDecisions();
2093
+ decisions.decisions = decisions.decisions.filter((decision) => decision.itemId !== item.id);
2094
+ decisions.decisions.push({ itemId: item.id, status, updatedAt: new Date().toISOString() });
2095
+ mkdirSync4(dirname4(decisionsPath), { recursive: true });
2096
+ writeFileSync4(decisionsPath, JSON.stringify(decisions, null, 2) + `
2097
+ `, "utf-8");
2098
+ await renderInk(/* @__PURE__ */ jsxDEV2(DecisionSaved, {
2099
+ item,
2100
+ status
2101
+ }, undefined, false, undefined, this));
2102
+ });
2103
+ program.command("summary").description("Print the last scan summary").option("--json", "print JSON instead of Ink output").action(async (options) => {
2104
+ const inventory = readInventory();
2105
+ if (options.json)
2106
+ return printJson(inventory.summary);
2107
+ await renderInk(/* @__PURE__ */ jsxDEV2(ScanSummary, {
2108
+ inventory,
2109
+ path: inventoryPath
2110
+ }, undefined, false, undefined, this));
2111
+ });
2112
+ program.command("doctor").description("Check Atelier CLI, UI, inventory, and local environment health").option("--json", "print JSON instead of Ink output").action(async (options) => {
2113
+ const checks = runDoctorChecks();
2114
+ if (options.json)
2115
+ printJson(checks);
2116
+ else
2117
+ await renderInk(/* @__PURE__ */ jsxDEV2(DoctorView, {
2118
+ checks
2119
+ }, undefined, false, undefined, this));
2120
+ if (checks.some((check) => check.status === "fail"))
2121
+ process.exitCode = 1;
2122
+ });
2123
+ program.parseAsync(process.argv).catch((error) => {
2124
+ console.error(error instanceof Error ? error.message : error);
2125
+ process.exit(1);
2126
+ });
2127
+ function printJson(value) {
2128
+ console.log(JSON.stringify(value, null, 2));
2129
+ }
2130
+ function readInventory() {
2131
+ if (!existsSync4(inventoryPath)) {
2132
+ throw new Error("No inventory found. Run `atl scan` first.");
2133
+ }
2134
+ return JSON.parse(readFileSync4(inventoryPath, "utf-8"));
2135
+ }
2136
+ function readDecisions() {
2137
+ if (!existsSync4(decisionsPath))
2138
+ return { version: 1, decisions: [] };
2139
+ return JSON.parse(readFileSync4(decisionsPath, "utf-8"));
2140
+ }
2141
+ function getStateDir() {
2142
+ if (process.env.ATELIER_STATE_DIR)
2143
+ return process.env.ATELIER_STATE_DIR;
2144
+ if (process.env.XDG_STATE_HOME)
2145
+ return join4(process.env.XDG_STATE_HOME, "atelier");
2146
+ if (platform3() === "win32" && process.env.LOCALAPPDATA)
2147
+ return join4(process.env.LOCALAPPDATA, "Atelier");
2148
+ return join4(homedir4(), ".local", "state", "atelier");
2149
+ }
2150
+ async function saveInventoryItem(query) {
2151
+ const inventory = readInventory();
2152
+ const item = findItem(inventory, query);
2153
+ if (!item) {
2154
+ await renderInk(/* @__PURE__ */ jsxDEV2(NotFound, {
2155
+ query
2156
+ }, undefined, false, undefined, this));
2157
+ process.exitCode = 1;
2158
+ return;
2159
+ }
2160
+ const vault2 = await getVaultState();
2161
+ let passphrase;
2162
+ if (!vault2.initialized) {
2163
+ passphrase = await readConfirmedPassphrase("Create an Atelier vault passphrase: ");
2164
+ } else if (!vault2.unlocked) {
2165
+ passphrase = await readHiddenLine("Vault passphrase: ");
2166
+ }
2167
+ const result = await saveConfigItem(item, { passphrase });
2168
+ await renderInk(/* @__PURE__ */ jsxDEV2(ConfigSavedView, {
2169
+ stableId: result.stableId,
2170
+ profileName: result.profileName,
2171
+ versionHash: result.version.contentSha256,
2172
+ sizeBytes: result.version.sizeBytes,
2173
+ reused: result.reused
2174
+ }, undefined, false, undefined, this));
2175
+ }
2176
+ async function readConfirmedPassphrase(prompt) {
2177
+ const passphrase = await readHiddenLine(prompt);
2178
+ const confirmation = await readHiddenLine("Confirm vault passphrase: ");
2179
+ if (passphrase !== confirmation)
2180
+ throw new Error("Vault passphrases do not match");
2181
+ return passphrase;
2182
+ }
2183
+ async function readHiddenLine(prompt) {
2184
+ if (!input.isTTY) {
2185
+ output.write(prompt);
2186
+ pipedInputLines ??= readFileSync4(0, "utf-8").split(/\r?\n/);
2187
+ return pipedInputLines.shift() ?? "";
2188
+ }
2189
+ output.write(prompt);
2190
+ input.setRawMode(true);
2191
+ input.resume();
2192
+ input.setEncoding("utf-8");
2193
+ let value = "";
2194
+ try {
2195
+ for await (const chunk of input) {
2196
+ const text = String(chunk);
2197
+ for (const char of text) {
2198
+ if (char === "\x03") {
2199
+ output.write(`
2200
+ `);
2201
+ process.exit(130);
2202
+ }
2203
+ if (char === "\r" || char === `
2204
+ `) {
2205
+ output.write(`
2206
+ `);
2207
+ return value;
2208
+ }
2209
+ if (char === "\x7F") {
2210
+ value = value.slice(0, -1);
2211
+ continue;
2212
+ }
2213
+ value += char;
2214
+ }
2215
+ }
2216
+ } finally {
2217
+ input.setRawMode(false);
2218
+ input.pause();
2219
+ }
2220
+ return value;
2221
+ }
2222
+ function filterItems(items, options) {
2223
+ return items.filter((item) => {
2224
+ if (options.domain && item.domain.toLowerCase() !== options.domain.toLowerCase())
2225
+ return false;
2226
+ if (options.app && item.app.toLowerCase() !== options.app.toLowerCase())
2227
+ return false;
2228
+ if (options.secrets && item.secretFindings.length === 0)
2229
+ return false;
2230
+ if (options.drift && !item.mirrors.some((mirror) => mirror.exists && mirror.identical === false))
2231
+ return false;
2232
+ if (options.generated && item.kind !== "generated")
2233
+ return false;
2234
+ if (options.private && item.shareability !== "private")
2235
+ return false;
2236
+ if (options.missing && item.exists)
2237
+ return false;
2238
+ return true;
2239
+ });
2240
+ }
2241
+ function findItem(inventory, query) {
2242
+ const normalized = normalizeQuery(query);
2243
+ return inventory.items.find((item) => item.id === query || normalizeQuery(item.path) === normalized || normalizeQuery(item.displayPath) === normalized) ?? inventory.items.find((item) => item.displayPath.includes(query) || item.path.includes(query));
2244
+ }
2245
+ function normalizeQuery(query) {
2246
+ return query.replace(/^~(?=\/|$)/, process.env.HOME ?? "").toLowerCase();
2247
+ }
2248
+ function runDoctorChecks() {
2249
+ const checks = [];
2250
+ const inventory = existsSync4(inventoryPath) ? readInventory() : undefined;
2251
+ const bunVersion = commandOutput("bun", ["--version"]);
2252
+ const nodeVersion = commandOutput("node", ["--version"]);
2253
+ const atlPath = commandOutput("which", ["atl"]);
2254
+ const gitDirty = commandOutput("git", ["status", "--porcelain"], repoRoot);
2255
+ checks.push({
2256
+ label: "repo",
2257
+ status: existsSync4(join4(repoRoot, "package.json")) ? "pass" : "fail",
2258
+ detail: repoRoot
2259
+ });
2260
+ checks.push({
2261
+ label: "bun",
2262
+ status: bunVersion ? "pass" : "fail",
2263
+ detail: bunVersion ? `v${bunVersion}` : "not found"
2264
+ });
2265
+ checks.push({
2266
+ label: "node for Vite",
2267
+ status: nodeVersion && isViteSupportedNode(nodeVersion) ? "pass" : "warn",
2268
+ detail: nodeVersion ? `${nodeVersion}${isViteSupportedNode(nodeVersion) ? "" : " \u2014 Vite wants 20.19+ or 22.12+"}` : "not found"
2269
+ });
2270
+ checks.push({
2271
+ label: "global atl",
2272
+ status: atlPath ? atlPath.includes(".bun") || atlPath.includes(repoRoot) ? "pass" : "warn" : "fail",
2273
+ detail: atlPath ?? "not linked; run `bun link` from the repo"
2274
+ });
2275
+ checks.push({
2276
+ label: "inventory",
2277
+ status: inventory ? "pass" : "warn",
2278
+ detail: inventory ? `${inventory.summary.existing}/${inventory.summary.total} existing, generated ${inventory.generatedAt}` : "missing; run `atl scan`"
2279
+ });
2280
+ checks.push({
2281
+ label: "secret warnings",
2282
+ status: inventory && inventory.summary.secrets > 0 ? "warn" : "pass",
2283
+ detail: inventory ? `${inventory.summary.secrets} item${inventory.summary.secrets === 1 ? "" : "s"} flagged` : "no inventory"
2284
+ });
2285
+ checks.push({
2286
+ label: "ui dependencies",
2287
+ status: existsSync4(join4(repoRoot, "node_modules", "@tanstack", "react-start")) ? "pass" : "fail",
2288
+ detail: existsSync4(join4(repoRoot, "node_modules", "@tanstack", "react-start")) ? "installed" : "missing; run `bun install`"
2289
+ });
2290
+ checks.push({
2291
+ label: "git state",
2292
+ status: gitDirty === undefined ? "warn" : gitDirty.trim() ? "warn" : "pass",
2293
+ detail: gitDirty === undefined ? "not a git repo" : gitDirty.trim() ? `${gitDirty.trim().split(`
2294
+ `).length} changed file(s)` : "clean"
2295
+ });
2296
+ return checks;
2297
+ }
2298
+ function commandOutput(command, args, cwd = repoRoot) {
2299
+ try {
2300
+ return execFileSync4(command, args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2301
+ } catch {
2302
+ return;
2303
+ }
2304
+ }
2305
+ function isViteSupportedNode(version) {
2306
+ const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)/);
2307
+ if (!match)
2308
+ return false;
2309
+ const [, majorRaw, minorRaw] = match;
2310
+ const major = Number(majorRaw);
2311
+ const minor = Number(minorRaw);
2312
+ if (major === 20)
2313
+ return minor >= 19;
2314
+ if (major === 22)
2315
+ return minor >= 12;
2316
+ return major > 22;
2317
+ }
2318
+ async function startApi(port) {
2319
+ const apiSecret = process.env.ATELIER_API_SECRET ?? crypto.randomUUID();
2320
+ const proc = Bun.spawn(["bun", "src/index.ts"], {
2321
+ cwd: join4(repoRoot, "apps", "api"),
2322
+ env: { ...process.env, PORT: String(port), ATELIER_API_SECRET: apiSecret },
2323
+ stdin: "inherit",
2324
+ stdout: "inherit",
2325
+ stderr: "inherit"
2326
+ });
2327
+ console.log(`Atelier API: http://localhost:${port}`);
2328
+ const code = await proc.exited;
2329
+ process.exit(code);
2330
+ }
2331
+ async function startUi(port) {
2332
+ const proc = Bun.spawn(["bun", "--bun", "vite", "dev", "--host", "127.0.0.1", "--port", String(port)], {
2333
+ cwd: join4(repoRoot, "apps", "ui"),
2334
+ env: { ...process.env, ATELIER_REPO_ROOT: repoRoot },
2335
+ stdin: "inherit",
2336
+ stdout: "inherit",
2337
+ stderr: "inherit"
2338
+ });
2339
+ console.log(`Atelier UI: http://localhost:${port}`);
2340
+ const code = await proc.exited;
2341
+ process.exit(code);
2342
+ }