@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.
- package/README.md +11 -0
- package/dist/index.js +2342 -0
- 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
|
+
}
|