@nocoo/otter 1.0.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/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +5 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +365 -0
- package/dist/cli.js.map +1 -0
- package/dist/collectors/applications.d.ts +19 -0
- package/dist/collectors/applications.d.ts.map +1 -0
- package/dist/collectors/applications.js +51 -0
- package/dist/collectors/applications.js.map +1 -0
- package/dist/collectors/base.d.ts +52 -0
- package/dist/collectors/base.d.ts.map +1 -0
- package/dist/collectors/base.js +186 -0
- package/dist/collectors/base.js.map +1 -0
- package/dist/collectors/claude-config.d.ts +39 -0
- package/dist/collectors/claude-config.d.ts.map +1 -0
- package/dist/collectors/claude-config.js +124 -0
- package/dist/collectors/claude-config.js.map +1 -0
- package/dist/collectors/homebrew.d.ts +16 -0
- package/dist/collectors/homebrew.d.ts.map +1 -0
- package/dist/collectors/homebrew.js +43 -0
- package/dist/collectors/homebrew.js.map +1 -0
- package/dist/collectors/index.d.ts +21 -0
- package/dist/collectors/index.d.ts.map +1 -0
- package/dist/collectors/index.js +28 -0
- package/dist/collectors/index.js.map +1 -0
- package/dist/collectors/opencode-config.d.ts +21 -0
- package/dist/collectors/opencode-config.d.ts.map +1 -0
- package/dist/collectors/opencode-config.js +88 -0
- package/dist/collectors/opencode-config.js.map +1 -0
- package/dist/collectors/shell-config.d.ts +24 -0
- package/dist/collectors/shell-config.d.ts.map +1 -0
- package/dist/collectors/shell-config.js +133 -0
- package/dist/collectors/shell-config.js.map +1 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +21 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/scan.d.ts +11 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +36 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/snapshot.d.ts +52 -0
- package/dist/commands/snapshot.d.ts.map +1 -0
- package/dist/commands/snapshot.js +203 -0
- package/dist/commands/snapshot.js.map +1 -0
- package/dist/config/manager.d.ts +13 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/config/manager.js +28 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/snapshot/builder.d.ts +7 -0
- package/dist/snapshot/builder.d.ts.map +1 -0
- package/dist/snapshot/builder.js +68 -0
- package/dist/snapshot/builder.js.map +1 -0
- package/dist/storage/local.d.ts +44 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/local.js +107 -0
- package/dist/storage/local.js.map +1 -0
- package/dist/uploader/icons.d.ts +41 -0
- package/dist/uploader/icons.d.ts.map +1 -0
- package/dist/uploader/icons.js +80 -0
- package/dist/uploader/icons.js.map +1 -0
- package/dist/uploader/webhook.d.ts +7 -0
- package/dist/uploader/webhook.d.ts.map +1 -0
- package/dist/uploader/webhook.js +54 -0
- package/dist/uploader/webhook.js.map +1 -0
- package/dist/utils/icons.d.ts +34 -0
- package/dist/utils/icons.d.ts.map +1 -0
- package/dist/utils/icons.js +113 -0
- package/dist/utils/icons.js.map +1 -0
- package/dist/utils/redact.d.ts +41 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +317 -0
- package/dist/utils/redact.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { hostname, platform, release, arch, userInfo, homedir } from "node:os";
|
|
4
|
+
/**
|
|
5
|
+
* Get the user-friendly computer name on macOS via `scutil --get ComputerName`.
|
|
6
|
+
* Returns undefined on non-macOS platforms or if the command fails.
|
|
7
|
+
*/
|
|
8
|
+
function getComputerName() {
|
|
9
|
+
if (platform() !== "darwin")
|
|
10
|
+
return undefined;
|
|
11
|
+
try {
|
|
12
|
+
return execSync("scutil --get ComputerName", { encoding: "utf-8" }).trim();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Gather current machine information for the snapshot.
|
|
20
|
+
*/
|
|
21
|
+
function getMachineInfo() {
|
|
22
|
+
const user = userInfo();
|
|
23
|
+
return {
|
|
24
|
+
hostname: hostname(),
|
|
25
|
+
computerName: getComputerName(),
|
|
26
|
+
platform: platform(),
|
|
27
|
+
osVersion: release(),
|
|
28
|
+
arch: arch(),
|
|
29
|
+
username: user.username,
|
|
30
|
+
homeDir: homedir(),
|
|
31
|
+
nodeVersion: process.version,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run a single collector, catching any crashes and converting
|
|
36
|
+
* them into error entries within the result.
|
|
37
|
+
*/
|
|
38
|
+
async function runCollector(collector) {
|
|
39
|
+
try {
|
|
40
|
+
return await collector.collect();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
return {
|
|
44
|
+
id: collector.id,
|
|
45
|
+
label: collector.label,
|
|
46
|
+
category: collector.category,
|
|
47
|
+
files: [],
|
|
48
|
+
lists: [],
|
|
49
|
+
errors: [`Collector '${collector.id}' crashed: ${err.message}`],
|
|
50
|
+
durationMs: 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build a complete snapshot by running all provided collectors
|
|
56
|
+
* and assembling results into the unified Snapshot format.
|
|
57
|
+
*/
|
|
58
|
+
export async function buildSnapshot(collectors) {
|
|
59
|
+
const results = await Promise.all(collectors.map(runCollector));
|
|
60
|
+
return {
|
|
61
|
+
version: 1,
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
id: randomUUID(),
|
|
64
|
+
machine: getMachineInfo(),
|
|
65
|
+
collectors: results,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.js","sourceRoot":"","sources":["../../src/snapshot/builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAG/E;;;GAGG;AACH,SAAS,eAAe;IACtB,IAAI,QAAQ,EAAE,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC9C,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,cAAc;IACrB,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;IACxB,OAAO;QACL,QAAQ,EAAE,QAAQ,EAAE;QACpB,YAAY,EAAE,eAAe,EAAE;QAC/B,QAAQ,EAAE,QAAQ,EAAE;QACpB,SAAS,EAAE,OAAO,EAAE;QACpB,IAAI,EAAE,IAAI,EAAE;QACZ,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,OAAO,EAAE,OAAO,EAAE;QAClB,WAAW,EAAE,OAAO,CAAC,OAAO;KAC7B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,YAAY,CAAC,SAAoB;IAC9C,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,CAAC,OAAO,EAAE,CAAC;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,SAAS,CAAC,EAAE;YAChB,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,QAAQ,EAAE,SAAS,CAAC,QAAQ;YAC5B,KAAK,EAAE,EAAE;YACT,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,CAAC,cAAc,SAAS,CAAC,EAAE,cAAe,GAAa,CAAC,OAAO,EAAE,CAAC;YAC1E,UAAU,EAAE,CAAC;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAAuB;IAEvB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IAEhE,OAAO;QACL,OAAO,EAAE,CAAC;QACV,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,EAAE,EAAE,UAAU,EAAE;QAChB,OAAO,EAAE,cAAc,EAAE;QACzB,UAAU,EAAE,OAAO;KACpB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Snapshot } from "@otter/core";
|
|
2
|
+
/** Metadata for a locally-stored snapshot (without loading full content) */
|
|
3
|
+
export interface SnapshotMeta {
|
|
4
|
+
/** Full UUID */
|
|
5
|
+
id: string;
|
|
6
|
+
/** First 8 characters of UUID — used as short reference */
|
|
7
|
+
shortId: string;
|
|
8
|
+
/** ISO 8601 creation timestamp */
|
|
9
|
+
createdAt: string;
|
|
10
|
+
/** Filename on disk */
|
|
11
|
+
filename: string;
|
|
12
|
+
/** File size in bytes */
|
|
13
|
+
sizeBytes: number;
|
|
14
|
+
/** Number of collectors in the snapshot */
|
|
15
|
+
collectorCount: number;
|
|
16
|
+
/** Total collected files across all collectors */
|
|
17
|
+
fileCount: number;
|
|
18
|
+
/** Total list items across all collectors */
|
|
19
|
+
listCount: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Local snapshot storage manager.
|
|
23
|
+
* Persists snapshots as JSON files in a dedicated directory.
|
|
24
|
+
*/
|
|
25
|
+
export declare class SnapshotStore {
|
|
26
|
+
private readonly storageDir;
|
|
27
|
+
constructor(storageDir: string);
|
|
28
|
+
/**
|
|
29
|
+
* Save a snapshot to local storage.
|
|
30
|
+
* Creates the storage directory if it doesn't exist.
|
|
31
|
+
* @returns The filename of the saved snapshot.
|
|
32
|
+
*/
|
|
33
|
+
save(snapshot: Snapshot): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* List all locally-stored snapshots, sorted by creation date (newest first).
|
|
36
|
+
*/
|
|
37
|
+
list(): Promise<SnapshotMeta[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Load a snapshot by its short ID prefix (first 8 chars of UUID).
|
|
40
|
+
* Returns null if no matching snapshot is found.
|
|
41
|
+
*/
|
|
42
|
+
load(shortId: string): Promise<Snapshot | null>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/storage/local.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,gBAAgB;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,cAAc,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAwDD;;;GAGG;AACH,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,MAAM;IAE/C;;;;OAIG;IACG,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ/C;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IA6BrC;;;OAGG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;CAiBtD"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFile, writeFile, readdir, mkdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Convert an ISO 8601 timestamp to a filesystem-safe string.
|
|
5
|
+
* Replaces colons with dashes to avoid issues on Windows.
|
|
6
|
+
* Example: "2026-03-06T12:30:00.000Z" → "2026-03-06T12-30-00"
|
|
7
|
+
*/
|
|
8
|
+
function toFileTimestamp(iso) {
|
|
9
|
+
return iso.replace(/:/g, "-").replace(/\.\d{3}Z$/, "");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build the snapshot filename from its metadata.
|
|
13
|
+
* Format: `{timestamp}_{shortId}.json`
|
|
14
|
+
*/
|
|
15
|
+
function buildFilename(snapshot) {
|
|
16
|
+
const ts = toFileTimestamp(snapshot.createdAt);
|
|
17
|
+
const shortId = snapshot.id.slice(0, 8);
|
|
18
|
+
return `${ts}_${shortId}.json`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse snapshot metadata from a stored JSON file without loading
|
|
22
|
+
* the full collector content into memory. Reads only what's needed.
|
|
23
|
+
*/
|
|
24
|
+
async function parseMetaFromFile(filePath, filename) {
|
|
25
|
+
try {
|
|
26
|
+
const info = await stat(filePath);
|
|
27
|
+
const raw = await readFile(filePath, "utf-8");
|
|
28
|
+
const snapshot = JSON.parse(raw);
|
|
29
|
+
return {
|
|
30
|
+
id: snapshot.id,
|
|
31
|
+
shortId: snapshot.id.slice(0, 8),
|
|
32
|
+
createdAt: snapshot.createdAt,
|
|
33
|
+
filename,
|
|
34
|
+
sizeBytes: info.size,
|
|
35
|
+
collectorCount: snapshot.collectors.length,
|
|
36
|
+
fileCount: snapshot.collectors.reduce((sum, c) => sum + c.files.length, 0),
|
|
37
|
+
listCount: snapshot.collectors.reduce((sum, c) => sum + c.lists.length, 0),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Corrupt or unreadable file — skip it
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Local snapshot storage manager.
|
|
47
|
+
* Persists snapshots as JSON files in a dedicated directory.
|
|
48
|
+
*/
|
|
49
|
+
export class SnapshotStore {
|
|
50
|
+
storageDir;
|
|
51
|
+
constructor(storageDir) {
|
|
52
|
+
this.storageDir = storageDir;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Save a snapshot to local storage.
|
|
56
|
+
* Creates the storage directory if it doesn't exist.
|
|
57
|
+
* @returns The filename of the saved snapshot.
|
|
58
|
+
*/
|
|
59
|
+
async save(snapshot) {
|
|
60
|
+
await mkdir(this.storageDir, { recursive: true });
|
|
61
|
+
const filename = buildFilename(snapshot);
|
|
62
|
+
const filePath = join(this.storageDir, filename);
|
|
63
|
+
await writeFile(filePath, JSON.stringify(snapshot, null, 2) + "\n");
|
|
64
|
+
return filename;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* List all locally-stored snapshots, sorted by creation date (newest first).
|
|
68
|
+
*/
|
|
69
|
+
async list() {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(this.storageDir);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Directory doesn't exist yet — no snapshots stored
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const jsonFiles = entries.filter((f) => f.endsWith(".json"));
|
|
79
|
+
const metas = [];
|
|
80
|
+
for (const filename of jsonFiles) {
|
|
81
|
+
const meta = await parseMetaFromFile(join(this.storageDir, filename), filename);
|
|
82
|
+
if (meta)
|
|
83
|
+
metas.push(meta);
|
|
84
|
+
}
|
|
85
|
+
// Sort newest first
|
|
86
|
+
metas.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
87
|
+
return metas;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Load a snapshot by its short ID prefix (first 8 chars of UUID).
|
|
91
|
+
* Returns null if no matching snapshot is found.
|
|
92
|
+
*/
|
|
93
|
+
async load(shortId) {
|
|
94
|
+
const metas = await this.list();
|
|
95
|
+
const match = metas.find((m) => m.shortId === shortId || m.id === shortId);
|
|
96
|
+
if (!match)
|
|
97
|
+
return null;
|
|
98
|
+
try {
|
|
99
|
+
const raw = await readFile(join(this.storageDir, match.filename), "utf-8");
|
|
100
|
+
return JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/storage/local.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Configuration for icon uploads to R2 */
|
|
2
|
+
export interface IconUploadConfig {
|
|
3
|
+
r2Endpoint: string;
|
|
4
|
+
r2AccessKeyId: string;
|
|
5
|
+
r2SecretAccessKey: string;
|
|
6
|
+
r2Bucket: string;
|
|
7
|
+
/** Public domain for the R2 bucket (e.g. "https://s.zhe.to") */
|
|
8
|
+
r2PublicDomain: string;
|
|
9
|
+
/** R2 key prefix (default: "apps/otter") */
|
|
10
|
+
prefix?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Result of uploading a single icon */
|
|
13
|
+
export interface IconUploadResult {
|
|
14
|
+
appName: string;
|
|
15
|
+
key: string;
|
|
16
|
+
publicUrl: string;
|
|
17
|
+
/** Whether the file was actually uploaded (false = already existed) */
|
|
18
|
+
uploaded: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate a deterministic hash from an app name.
|
|
22
|
+
* Returns the first 12 hex chars of SHA-256(appName).
|
|
23
|
+
*/
|
|
24
|
+
export declare function hashAppName(appName: string): string;
|
|
25
|
+
/** Reset the cached S3 client (for testing). */
|
|
26
|
+
export declare function resetIconUploadClient(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Upload a single icon PNG to R2.
|
|
29
|
+
* Key format: {prefix}/{hash}.png
|
|
30
|
+
* Skips upload if the object already exists (same app name = same key).
|
|
31
|
+
*/
|
|
32
|
+
export declare function uploadIcon(pngPath: string, appName: string, config: IconUploadConfig): Promise<IconUploadResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Upload multiple icon PNGs to R2 in sequence.
|
|
35
|
+
* Returns results for all successful exports.
|
|
36
|
+
*/
|
|
37
|
+
export declare function uploadIcons(icons: Array<{
|
|
38
|
+
appName: string;
|
|
39
|
+
pngPath: string;
|
|
40
|
+
}>, config: IconUploadConfig, onProgress?: (result: IconUploadResult) => void): Promise<IconUploadResult[]>;
|
|
41
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../src/uploader/icons.ts"],"names":[],"mappings":"AAIA,2CAA2C;AAC3C,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,cAAc,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAiBD,gDAAgD;AAChD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAkBD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,gBAAgB,CAAC,CA2B3B;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,MAAM,EAAE,gBAAgB,EACxB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAC9C,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAU7B"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { S3Client, PutObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3";
|
|
4
|
+
/**
|
|
5
|
+
* Generate a deterministic hash from an app name.
|
|
6
|
+
* Returns the first 12 hex chars of SHA-256(appName).
|
|
7
|
+
*/
|
|
8
|
+
export function hashAppName(appName) {
|
|
9
|
+
return createHash("sha256").update(appName).digest("hex").slice(0, 12);
|
|
10
|
+
}
|
|
11
|
+
let _client = null;
|
|
12
|
+
function getClient(config) {
|
|
13
|
+
if (_client)
|
|
14
|
+
return _client;
|
|
15
|
+
_client = new S3Client({
|
|
16
|
+
region: "auto",
|
|
17
|
+
endpoint: config.r2Endpoint,
|
|
18
|
+
credentials: {
|
|
19
|
+
accessKeyId: config.r2AccessKeyId,
|
|
20
|
+
secretAccessKey: config.r2SecretAccessKey,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return _client;
|
|
24
|
+
}
|
|
25
|
+
/** Reset the cached S3 client (for testing). */
|
|
26
|
+
export function resetIconUploadClient() {
|
|
27
|
+
_client = null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if an object already exists in R2.
|
|
31
|
+
*/
|
|
32
|
+
async function objectExists(client, bucket, key) {
|
|
33
|
+
try {
|
|
34
|
+
await client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Upload a single icon PNG to R2.
|
|
43
|
+
* Key format: {prefix}/{hash}.png
|
|
44
|
+
* Skips upload if the object already exists (same app name = same key).
|
|
45
|
+
*/
|
|
46
|
+
export async function uploadIcon(pngPath, appName, config) {
|
|
47
|
+
const prefix = config.prefix ?? "apps/otter";
|
|
48
|
+
const hash = hashAppName(appName);
|
|
49
|
+
const key = `${prefix}/${hash}.png`;
|
|
50
|
+
const publicUrl = `${config.r2PublicDomain}/${key}`;
|
|
51
|
+
const client = getClient(config);
|
|
52
|
+
// Skip if already uploaded (same app name = same hash = same key)
|
|
53
|
+
const exists = await objectExists(client, config.r2Bucket, key);
|
|
54
|
+
if (exists) {
|
|
55
|
+
return { appName, key, publicUrl, uploaded: false };
|
|
56
|
+
}
|
|
57
|
+
const body = await readFile(pngPath);
|
|
58
|
+
await client.send(new PutObjectCommand({
|
|
59
|
+
Bucket: config.r2Bucket,
|
|
60
|
+
Key: key,
|
|
61
|
+
Body: body,
|
|
62
|
+
ContentType: "image/png",
|
|
63
|
+
CacheControl: "public, max-age=31536000, immutable",
|
|
64
|
+
}));
|
|
65
|
+
return { appName, key, publicUrl, uploaded: true };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Upload multiple icon PNGs to R2 in sequence.
|
|
69
|
+
* Returns results for all successful exports.
|
|
70
|
+
*/
|
|
71
|
+
export async function uploadIcons(icons, config, onProgress) {
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const { appName, pngPath } of icons) {
|
|
74
|
+
const result = await uploadIcon(pngPath, appName, config);
|
|
75
|
+
results.push(result);
|
|
76
|
+
onProgress?.(result);
|
|
77
|
+
}
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=icons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.js","sourceRoot":"","sources":["../../src/uploader/icons.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Snapshot, UploaderConfig, UploadResult } from "@otter/core";
|
|
2
|
+
/**
|
|
3
|
+
* Upload a snapshot to the configured webhook URL via HTTP POST.
|
|
4
|
+
* The payload is gzip-compressed to reduce transfer size.
|
|
5
|
+
*/
|
|
6
|
+
export declare function uploadSnapshot(snapshot: Snapshot, config: UploaderConfig): Promise<UploadResult>;
|
|
7
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/uploader/webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI1E;;;GAGG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,YAAY,CAAC,CAoDvB"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { gzipSync } from "node:zlib";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
|
+
/**
|
|
4
|
+
* Upload a snapshot to the configured webhook URL via HTTP POST.
|
|
5
|
+
* The payload is gzip-compressed to reduce transfer size.
|
|
6
|
+
*/
|
|
7
|
+
export async function uploadSnapshot(snapshot, config) {
|
|
8
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
|
+
const start = performance.now();
|
|
12
|
+
try {
|
|
13
|
+
const jsonBody = JSON.stringify(snapshot);
|
|
14
|
+
const compressed = gzipSync(jsonBody);
|
|
15
|
+
const response = await fetch(config.webhookUrl, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
"Content-Encoding": "gzip",
|
|
20
|
+
},
|
|
21
|
+
body: compressed,
|
|
22
|
+
signal: controller.signal,
|
|
23
|
+
});
|
|
24
|
+
const durationMs = Math.round(performance.now() - start);
|
|
25
|
+
if (response.ok) {
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
statusCode: response.status,
|
|
29
|
+
durationMs,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
statusCode: response.status,
|
|
35
|
+
error: `Upload failed with status ${response.status} ${response.statusText}`,
|
|
36
|
+
durationMs,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const durationMs = Math.round(performance.now() - start);
|
|
41
|
+
const message = err instanceof DOMException && err.name === "AbortError"
|
|
42
|
+
? `Upload timed out after ${timeoutMs}ms`
|
|
43
|
+
: err.message;
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
error: message,
|
|
47
|
+
durationMs,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/uploader/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGrC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,QAAkB,EAClB,MAAsB;IAEtB,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,kBAAkB,CAAC;IACzD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAE9D,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAEhC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAEtC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,UAAU,EAAE;YAC9C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,kBAAkB,EAAE,MAAM;aAC3B;YACD,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;QAEzD,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YAChB,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,UAAU,EAAE,QAAQ,CAAC,MAAM;gBAC3B,UAAU;aACX,CAAC;QACJ,CAAC;QAED,OAAO;YACL,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,QAAQ,CAAC,MAAM;YAC3B,KAAK,EAAE,6BAA6B,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE;YAC5E,UAAU;SACX,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;QACzD,MAAM,OAAO,GACX,GAAG,YAAY,YAAY,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY;YACtD,CAAC,CAAC,0BAA0B,SAAS,IAAI;YACzC,CAAC,CAAE,GAAa,CAAC,OAAO,CAAC;QAE7B,OAAO;YACL,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,OAAO;YACd,UAAU;SACX,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Result of extracting a single app icon */
|
|
2
|
+
export interface IconExportResult {
|
|
3
|
+
/** App name (without .app) */
|
|
4
|
+
appName: string;
|
|
5
|
+
/** Whether the icon was exported successfully */
|
|
6
|
+
success: boolean;
|
|
7
|
+
/** Output PNG path (when successful) */
|
|
8
|
+
outputPath?: string;
|
|
9
|
+
/** Error message (when failed) */
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Options for the export-icons operation */
|
|
13
|
+
export interface ExportIconsOptions {
|
|
14
|
+
/** Applications directory (default: /Applications) */
|
|
15
|
+
appsDir?: string;
|
|
16
|
+
/** Output directory for PNG icons */
|
|
17
|
+
outputDir: string;
|
|
18
|
+
/** Icon width in pixels (default: 128) */
|
|
19
|
+
size?: number;
|
|
20
|
+
/** Called after each icon is processed */
|
|
21
|
+
onProgress?: (result: IconExportResult) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract CFBundleIconFile from an XML Info.plist.
|
|
25
|
+
* Uses simple regex to avoid needing an XML parser dependency.
|
|
26
|
+
* Exported for testing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractIconFileName(plistContent: string): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Export app icons from /Applications as PNG files to a target directory.
|
|
31
|
+
* Uses macOS `sips` for conversion (no external dependencies).
|
|
32
|
+
*/
|
|
33
|
+
export declare function exportIcons(options: ExportIconsOptions): Promise<IconExportResult[]>;
|
|
34
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../src/utils/icons.ts"],"names":[],"mappings":"AAQA,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6CAA6C;AAC7C,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACjD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAavE;AA2CD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA8D7B"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readdir, readFile, mkdir, access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
/**
|
|
7
|
+
* Extract CFBundleIconFile from an XML Info.plist.
|
|
8
|
+
* Uses simple regex to avoid needing an XML parser dependency.
|
|
9
|
+
* Exported for testing.
|
|
10
|
+
*/
|
|
11
|
+
export function extractIconFileName(plistContent) {
|
|
12
|
+
// Match <key>CFBundleIconFile</key> followed by <string>value</string>
|
|
13
|
+
const pattern = /<key>CFBundleIconFile<\/key>\s*<string>([^<]+)<\/string>/;
|
|
14
|
+
const match = plistContent.match(pattern);
|
|
15
|
+
if (!match)
|
|
16
|
+
return null;
|
|
17
|
+
let iconFile = match[1].trim();
|
|
18
|
+
// Ensure .icns extension (some plists omit it)
|
|
19
|
+
if (!iconFile.endsWith(".icns")) {
|
|
20
|
+
iconFile += ".icns";
|
|
21
|
+
}
|
|
22
|
+
return iconFile;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the .icns file path for a given .app bundle.
|
|
26
|
+
* Returns null if the icon cannot be found.
|
|
27
|
+
*/
|
|
28
|
+
async function resolveIconPath(appPath) {
|
|
29
|
+
const plistPath = join(appPath, "Contents", "Info.plist");
|
|
30
|
+
try {
|
|
31
|
+
const plistContent = await readFile(plistPath, "utf-8");
|
|
32
|
+
const iconFile = extractIconFileName(plistContent);
|
|
33
|
+
if (!iconFile)
|
|
34
|
+
return null;
|
|
35
|
+
const iconPath = join(appPath, "Contents", "Resources", iconFile);
|
|
36
|
+
await access(iconPath);
|
|
37
|
+
return iconPath;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert a .icns file to .png using macOS built-in sips.
|
|
45
|
+
* Returns the output path on success, or throws on failure.
|
|
46
|
+
*/
|
|
47
|
+
async function convertIcnsToPng(icnsPath, outputPath, size) {
|
|
48
|
+
await execFileAsync("/usr/bin/sips", [
|
|
49
|
+
"-s",
|
|
50
|
+
"format",
|
|
51
|
+
"png",
|
|
52
|
+
"--resampleWidth",
|
|
53
|
+
String(size),
|
|
54
|
+
icnsPath,
|
|
55
|
+
"--out",
|
|
56
|
+
outputPath,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Export app icons from /Applications as PNG files to a target directory.
|
|
61
|
+
* Uses macOS `sips` for conversion (no external dependencies).
|
|
62
|
+
*/
|
|
63
|
+
export async function exportIcons(options) {
|
|
64
|
+
const { appsDir = "/Applications", outputDir, size = 128, onProgress, } = options;
|
|
65
|
+
// Ensure output directory exists
|
|
66
|
+
await mkdir(outputDir, { recursive: true });
|
|
67
|
+
const results = [];
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = await readdir(appsDir, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
const apps = entries.filter((e) => e.isDirectory() && e.name.endsWith(".app"));
|
|
76
|
+
for (const app of apps) {
|
|
77
|
+
const appName = app.name.replace(/\.app$/, "");
|
|
78
|
+
const appPath = join(appsDir, app.name);
|
|
79
|
+
const icnsPath = await resolveIconPath(appPath);
|
|
80
|
+
if (!icnsPath) {
|
|
81
|
+
const result = {
|
|
82
|
+
appName,
|
|
83
|
+
success: false,
|
|
84
|
+
error: "No icon found in Info.plist",
|
|
85
|
+
};
|
|
86
|
+
results.push(result);
|
|
87
|
+
onProgress?.(result);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const outputPath = join(outputDir, `${appName}.png`);
|
|
91
|
+
try {
|
|
92
|
+
await convertIcnsToPng(icnsPath, outputPath, size);
|
|
93
|
+
const result = {
|
|
94
|
+
appName,
|
|
95
|
+
success: true,
|
|
96
|
+
outputPath,
|
|
97
|
+
};
|
|
98
|
+
results.push(result);
|
|
99
|
+
onProgress?.(result);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const result = {
|
|
103
|
+
appName,
|
|
104
|
+
success: false,
|
|
105
|
+
error: `sips conversion failed: ${err.message}`,
|
|
106
|
+
};
|
|
107
|
+
results.push(result);
|
|
108
|
+
onProgress?.(result);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=icons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.js","sourceRoot":"","sources":["../../src/utils/icons.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction utilities.
|
|
3
|
+
*
|
|
4
|
+
* Strips sensitive values from file content before including
|
|
5
|
+
* them in snapshots. Works on both structured (JSON) and
|
|
6
|
+
* line-oriented (ini-like) config formats.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Redact sensitive values in a JSON string.
|
|
10
|
+
* Returns the original string if no redaction was needed or parsing fails.
|
|
11
|
+
*/
|
|
12
|
+
export declare function redactJsonSecrets(jsonContent: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Redact sensitive lines in ini-style / line-oriented config files.
|
|
15
|
+
* Each matching line has its value portion replaced with [REDACTED].
|
|
16
|
+
*/
|
|
17
|
+
export declare function redactLineSecrets(content: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Redact sensitive variable assignments in shell scripts.
|
|
20
|
+
* Catches patterns like:
|
|
21
|
+
* export MY_API_KEY="sk-..."
|
|
22
|
+
* GITHUB_TOKEN=ghp_xxx
|
|
23
|
+
* export Z_AI_API_KEY="..."
|
|
24
|
+
*/
|
|
25
|
+
export declare function redactShellSecrets(content: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Redact credential-like values in JSONL content (one JSON object per line).
|
|
28
|
+
* Applies both key-based (sensitive key names) and value-based (credential
|
|
29
|
+
* patterns like JWTs, API keys, bearer tokens) redaction.
|
|
30
|
+
*/
|
|
31
|
+
export declare function redactJsonlSecrets(content: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Redact credentials from file content.
|
|
34
|
+
* Auto-detects format based on file path extension or filename.
|
|
35
|
+
*
|
|
36
|
+
* @param content Raw file content
|
|
37
|
+
* @param filePath File path (used to determine format)
|
|
38
|
+
* @returns Content with sensitive values replaced by [REDACTED]
|
|
39
|
+
*/
|
|
40
|
+
export declare function redactSecrets(content: string, filePath: string): string;
|
|
41
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/utils/redact.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2DH;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAW7D;AAiDD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAazD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAiB1D;AA8FD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA2B1D;AAMD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8BvE"}
|