@hydra-acp/archiver 0.1.16 → 0.1.17
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/acp/attach.js +1 -201
- package/dist/acp/protocol.js +1 -13
- package/dist/archive-loop.js +1 -134
- package/dist/backend/encrypted.js +1 -59
- package/dist/backend/factory.js +1 -28
- package/dist/backend/fs.js +1 -78
- package/dist/backend/google-drive.js +1 -182
- package/dist/backend/s3.js +5 -189
- package/dist/backend/types.js +0 -2
- package/dist/bridge.js +1 -100
- package/dist/cold-sweep.js +1 -54
- package/dist/config.js +1 -159
- package/dist/daemon.js +1 -81
- package/dist/discovery.js +1 -82
- package/dist/envelope.js +1 -123
- package/dist/index.js +15 -241
- package/dist/keygen.js +8 -26
- package/dist/oauth/google.js +6 -195
- package/dist/pull-loop.js +1 -147
- package/dist/rule.js +1 -37
- package/dist/setup/conf-writer.js +4 -85
- package/dist/setup/downloads-scan.js +1 -44
- package/dist/setup/prompts.js +14 -123
- package/dist/setup/wizard.js +17 -415
- package/dist/state.js +1 -129
- package/dist/util/aws-credentials.js +1 -82
- package/dist/util/log.js +2 -46
- package/package.json +5 -4
- package/dist/acp/attach.js.map +0 -1
- package/dist/acp/protocol.js.map +0 -1
- package/dist/archive-loop.js.map +0 -1
- package/dist/backend/encrypted.js.map +0 -1
- package/dist/backend/factory.js.map +0 -1
- package/dist/backend/fs.js.map +0 -1
- package/dist/backend/google-drive.js.map +0 -1
- package/dist/backend/s3.js.map +0 -1
- package/dist/backend/types.js.map +0 -1
- package/dist/bridge.js.map +0 -1
- package/dist/cold-sweep.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/daemon.js.map +0 -1
- package/dist/discovery.js.map +0 -1
- package/dist/envelope.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keygen.js.map +0 -1
- package/dist/oauth/google.js.map +0 -1
- package/dist/pull-loop.js.map +0 -1
- package/dist/rule.js.map +0 -1
- package/dist/setup/conf-writer.js.map +0 -1
- package/dist/setup/downloads-scan.js.map +0 -1
- package/dist/setup/prompts.js.map +0 -1
- package/dist/setup/wizard.js.map +0 -1
- package/dist/state.js.map +0 -1
- package/dist/util/aws-credentials.js.map +0 -1
- package/dist/util/log.js.map +0 -1
package/dist/pull-loop.js
CHANGED
|
@@ -1,147 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { logger } from "./util/log.js";
|
|
3
|
-
const log = logger("pull");
|
|
4
|
-
export class PullLoop {
|
|
5
|
-
opts;
|
|
6
|
-
timer;
|
|
7
|
-
stopped = false;
|
|
8
|
-
inFlight = false;
|
|
9
|
-
constructor(opts) {
|
|
10
|
-
this.opts = opts;
|
|
11
|
-
}
|
|
12
|
-
start() {
|
|
13
|
-
log.info(`polling backend every ${this.opts.intervalMs}ms for peer-uploaded bundles`);
|
|
14
|
-
void this.tick();
|
|
15
|
-
this.timer = setInterval(() => {
|
|
16
|
-
void this.tick();
|
|
17
|
-
}, this.opts.intervalMs);
|
|
18
|
-
this.timer.unref();
|
|
19
|
-
}
|
|
20
|
-
stop() {
|
|
21
|
-
this.stopped = true;
|
|
22
|
-
if (this.timer) {
|
|
23
|
-
clearInterval(this.timer);
|
|
24
|
-
this.timer = undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
// tickNow is exposed for tests to drive a single iteration on demand.
|
|
28
|
-
async tickNow() {
|
|
29
|
-
await this.tick();
|
|
30
|
-
}
|
|
31
|
-
async tick() {
|
|
32
|
-
if (this.stopped || this.inFlight) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
this.inFlight = true;
|
|
36
|
-
try {
|
|
37
|
-
// Snapshot the daemon's session set once per tick. Any envelope
|
|
38
|
-
// whose inner bundle.session.sessionId is already present locally
|
|
39
|
-
// is one we (or a peer using our credentials) uploaded — skip it.
|
|
40
|
-
let localSessionIds;
|
|
41
|
-
try {
|
|
42
|
-
localSessionIds = await this.opts.daemon.listSessionIds();
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
log.warn(`session list failed; skipping tick: ${err.message}`);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
await this.resetDeletedImports(localSessionIds);
|
|
49
|
-
const ownPrefix = this.opts.hostId + "/";
|
|
50
|
-
const entries = await this.opts.backend.list();
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
if (this.stopped) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (entry.key.startsWith(ownPrefix)) {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
await this.processEntry(entry.key, localSessionIds).catch((err) => {
|
|
59
|
-
log.warn(`processing ${entry.key} failed: ${err.message}`);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
log.warn(`backend.list failed: ${err.message}`);
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
this.inFlight = false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
async resetDeletedImports(localSessionIds) {
|
|
71
|
-
for (const lineageId of this.opts.state.lineageIds()) {
|
|
72
|
-
const entry = this.opts.state.get(lineageId);
|
|
73
|
-
if (entry.importedSessionId !== undefined &&
|
|
74
|
-
!localSessionIds.has(entry.importedSessionId)) {
|
|
75
|
-
await this.opts.state.resetImport(lineageId);
|
|
76
|
-
log.info(`imported session for lineage=${lineageId} was deleted; will re-import`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async processEntry(key, localSessionIds) {
|
|
81
|
-
const raw = await this.opts.backend.get(key);
|
|
82
|
-
let envelope;
|
|
83
|
-
try {
|
|
84
|
-
envelope = deserialize(raw);
|
|
85
|
-
}
|
|
86
|
-
catch (err) {
|
|
87
|
-
log.warn(`malformed envelope at ${key}: ${err.message}`);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const { lineageId } = envelope;
|
|
91
|
-
// Self-loop suppression: if the bundle's sessionId already exists
|
|
92
|
-
// in the local daemon, we (or a peer reflecting our upload) made
|
|
93
|
-
// this — no need to import. This survives hostname changes and
|
|
94
|
-
// race-conditions with state writes that a hostname or hash check
|
|
95
|
-
// would not.
|
|
96
|
-
const innerSessionId = readSessionId(envelope.bundle);
|
|
97
|
-
if (innerSessionId !== undefined && localSessionIds.has(innerSessionId)) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const local = this.opts.state.get(lineageId);
|
|
101
|
-
if (local.lastSeenRemoteUploadedAt !== undefined &&
|
|
102
|
-
envelope.uploadedAt <= local.lastSeenRemoteUploadedAt) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (typeof envelope.bundle !== "object" ||
|
|
106
|
-
envelope.bundle === null) {
|
|
107
|
-
log.warn(`envelope ${key} bundle is not an object; skipping`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
log.info(`importing lineage=${lineageId} from ${envelope.uploadedBy.host} uploadedAt=${envelope.uploadedAt}`);
|
|
111
|
-
let importedSessionId;
|
|
112
|
-
try {
|
|
113
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
-
const result = await this.opts.daemon.importBundle(envelope.bundle, {
|
|
115
|
-
replace: true,
|
|
116
|
-
});
|
|
117
|
-
importedSessionId = result.sessionId;
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
log.warn(`daemon import for lineage ${lineageId} failed: ${err.message}`);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
// Suppress the immediate re-upload that the daemon's
|
|
124
|
-
// import-induced turn_complete (if any) would trigger: we record
|
|
125
|
-
// the envelope hash as our last-uploaded hash so the archive-loop
|
|
126
|
-
// sees "no change" on the next export.
|
|
127
|
-
await this.opts.state.set(lineageId, {
|
|
128
|
-
lastSeenRemoteUploadedAt: envelope.uploadedAt,
|
|
129
|
-
lastSeenRemoteBy: envelope.uploadedBy.host,
|
|
130
|
-
lastUploadedHash: envelope.bundleHash,
|
|
131
|
-
lastUploadedAt: envelope.uploadedAt,
|
|
132
|
-
importedSessionId,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function readSessionId(bundle) {
|
|
137
|
-
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
const session = bundle.session;
|
|
141
|
-
if (!session || typeof session !== "object" || Array.isArray(session)) {
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
const id = session.sessionId;
|
|
145
|
-
return typeof id === "string" ? id : undefined;
|
|
146
|
-
}
|
|
147
|
-
//# sourceMappingURL=pull-loop.js.map
|
|
1
|
+
import{deserialize as c}from"./envelope.js";import{logger as u}from"./util/log.js";const i=u("pull");class y{constructor(t){this.opts=t}opts;timer;stopped=!1;inFlight=!1;start(){i.info(`polling backend every ${this.opts.intervalMs}ms for peer-uploaded bundles`),this.tick(),this.timer=setInterval(()=>{this.tick()},this.opts.intervalMs),this.timer.unref()}stop(){this.stopped=!0,this.timer&&(clearInterval(this.timer),this.timer=void 0)}async tickNow(){await this.tick()}async tick(){if(!(this.stopped||this.inFlight)){this.inFlight=!0;try{let t;try{t=await this.opts.daemon.listSessionIds()}catch(e){i.warn(`session list failed; skipping tick: ${e.message}`);return}await this.resetDeletedImports(t);const s=this.opts.hostId+"/",n=await this.opts.backend.list();for(const e of n){if(this.stopped)return;e.key.startsWith(s)||await this.processEntry(e.key,t).catch(o=>{i.warn(`processing ${e.key} failed: ${o.message}`)})}}catch(t){i.warn(`backend.list failed: ${t.message}`)}finally{this.inFlight=!1}}}async resetDeletedImports(t){for(const s of this.opts.state.lineageIds()){const n=this.opts.state.get(s);n.importedSessionId!==void 0&&!t.has(n.importedSessionId)&&(await this.opts.state.resetImport(s),i.info(`imported session for lineage=${s} was deleted; will re-import`))}}async processEntry(t,s){const n=await this.opts.backend.get(t);let e;try{e=c(n)}catch(a){i.warn(`malformed envelope at ${t}: ${a.message}`);return}const{lineageId:o}=e,d=f(e.bundle);if(d!==void 0&&s.has(d))return;const l=this.opts.state.get(o);if(l.lastSeenRemoteUploadedAt!==void 0&&e.uploadedAt<=l.lastSeenRemoteUploadedAt)return;if(typeof e.bundle!="object"||e.bundle===null){i.warn(`envelope ${t} bundle is not an object; skipping`);return}i.info(`importing lineage=${o} from ${e.uploadedBy.host} uploadedAt=${e.uploadedAt}`);let p;try{p=(await this.opts.daemon.importBundle(e.bundle,{replace:!0})).sessionId}catch(a){i.warn(`daemon import for lineage ${o} failed: ${a.message}`);return}await this.opts.state.set(o,{lastSeenRemoteUploadedAt:e.uploadedAt,lastSeenRemoteBy:e.uploadedBy.host,lastUploadedHash:e.bundleHash,lastUploadedAt:e.uploadedAt,importedSessionId:p})}}function f(r){if(!r||typeof r!="object"||Array.isArray(r))return;const t=r.session;if(!t||typeof t!="object"||Array.isArray(t))return;const s=t.sessionId;return typeof s=="string"?s:void 0}export{y as PullLoop};
|
package/dist/rule.js
CHANGED
|
@@ -1,37 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { logger } from "./util/log.js";
|
|
4
|
-
const log = logger("rule");
|
|
5
|
-
export const DEFAULT_RULE = () => true;
|
|
6
|
-
let loadCounter = 0;
|
|
7
|
-
export async function loadRule(path) {
|
|
8
|
-
try {
|
|
9
|
-
await stat(path);
|
|
10
|
-
}
|
|
11
|
-
catch (err) {
|
|
12
|
-
const e = err;
|
|
13
|
-
if (e.code === "ENOENT") {
|
|
14
|
-
log.info(`no rule config at ${path} — archiving every live session (drop a JS file at that path to opt out specific sessions)`);
|
|
15
|
-
return DEFAULT_RULE;
|
|
16
|
-
}
|
|
17
|
-
log.warn(`stat ${path} failed: ${e.message}; using DEFAULT_RULE`);
|
|
18
|
-
return DEFAULT_RULE;
|
|
19
|
-
}
|
|
20
|
-
loadCounter += 1;
|
|
21
|
-
const url = `${pathToFileURL(path).href}?v=${Date.now()}-${loadCounter}`;
|
|
22
|
-
try {
|
|
23
|
-
const mod = (await import(url));
|
|
24
|
-
const fn = mod.default;
|
|
25
|
-
if (typeof fn !== "function") {
|
|
26
|
-
log.warn(`${path} did not export a default function; using DEFAULT_RULE`);
|
|
27
|
-
return DEFAULT_RULE;
|
|
28
|
-
}
|
|
29
|
-
log.info(`loaded archiver rule from ${path}`);
|
|
30
|
-
return fn;
|
|
31
|
-
}
|
|
32
|
-
catch (err) {
|
|
33
|
-
log.warn(`import ${path} failed: ${err.message}; using DEFAULT_RULE`);
|
|
34
|
-
return DEFAULT_RULE;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
//# sourceMappingURL=rule.js.map
|
|
1
|
+
import{stat as a}from"node:fs/promises";import{pathToFileURL as u}from"node:url";import{logger as c}from"./util/log.js";const t=c("rule"),r=()=>!0;let i=0;async function g(e){try{await a(e)}catch(o){const n=o;return n.code==="ENOENT"?(t.info(`no rule config at ${e} \u2014 archiving every live session (drop a JS file at that path to opt out specific sessions)`),r):(t.warn(`stat ${e} failed: ${n.message}; using DEFAULT_RULE`),r)}i+=1;const s=`${u(e).href}?v=${Date.now()}-${i}`;try{const n=(await import(s)).default;return typeof n!="function"?(t.warn(`${e} did not export a default function; using DEFAULT_RULE`),r):(t.info(`loaded archiver rule from ${e}`),n)}catch(o){return t.warn(`import ${e} failed: ${o.message}; using DEFAULT_RULE`),r}}export{r as DEFAULT_RULE,g as loadRule};
|
|
@@ -1,86 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
-
export const PRIMARY_CONF_PATH = resolve(homedir(), ".hydra-acp", "archiver.conf");
|
|
5
|
-
function parseLines(text) {
|
|
6
|
-
return text.split(/\r?\n/).map((raw) => {
|
|
7
|
-
const trimmed = raw.trim();
|
|
8
|
-
if (!trimmed || trimmed.startsWith("#"))
|
|
9
|
-
return { raw };
|
|
10
|
-
const eq = raw.indexOf("=");
|
|
11
|
-
if (eq === -1)
|
|
12
|
-
return { raw };
|
|
13
|
-
const key = raw.slice(0, eq).trim();
|
|
14
|
-
let value = raw.slice(eq + 1).trim();
|
|
15
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
16
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
17
|
-
value = value.slice(1, -1);
|
|
18
|
-
}
|
|
19
|
-
return { raw, key, value };
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
export function readExisting(path) {
|
|
23
|
-
if (!existsSync(path))
|
|
24
|
-
return { text: "", map: new Map() };
|
|
25
|
-
const text = readFileSync(path, "utf8");
|
|
26
|
-
const map = new Map();
|
|
27
|
-
for (const line of parseLines(text)) {
|
|
28
|
-
if (line.key !== undefined && line.value !== undefined)
|
|
29
|
-
map.set(line.key, line.value);
|
|
30
|
-
}
|
|
31
|
-
return { text, map };
|
|
32
|
-
}
|
|
33
|
-
const HEADER = `# hydra-acp-archiver config — written by 'hydra-acp-archiver setup'.
|
|
1
|
+
import{chmodSync as d,existsSync as u,mkdirSync as g,readFileSync as a,writeFileSync as p}from"node:fs";import{homedir as h}from"node:os";import{dirname as m,resolve as l}from"node:path";const S=l(h(),".hydra-acp","archiver.conf");function c(n){return n.split(/\r?\n/).map(r=>{const s=r.trim();if(!s||s.startsWith("#"))return{raw:r};const t=r.indexOf("=");if(t===-1)return{raw:r};const i=r.slice(0,t).trim();let e=r.slice(t+1).trim();return(e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'"))&&(e=e.slice(1,-1)),{raw:r,key:i,value:e}})}function y(n){if(!u(n))return{text:"",map:new Map};const r=a(n,"utf8"),s=new Map;for(const t of c(r))t.key!==void 0&&t.value!==void 0&&s.set(t.key,t.value);return{text:r,map:s}}const k=`# hydra-acp-archiver config \u2014 written by 'hydra-acp-archiver setup'.
|
|
34
2
|
# Lock this file: chmod 600.
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return `"${value.replace(/"/g, '\\"')}"`;
|
|
39
|
-
return value;
|
|
40
|
-
}
|
|
41
|
-
export function mergeConf(existing, updates) {
|
|
42
|
-
const lines = parseLines(existing);
|
|
43
|
-
const remaining = new Map();
|
|
44
|
-
for (const [k, v] of Object.entries(updates)) {
|
|
45
|
-
if (v !== undefined)
|
|
46
|
-
remaining.set(k, v);
|
|
47
|
-
}
|
|
48
|
-
const out = [];
|
|
49
|
-
for (const line of lines) {
|
|
50
|
-
if (line.key && remaining.has(line.key)) {
|
|
51
|
-
const newVal = remaining.get(line.key);
|
|
52
|
-
out.push(`${line.key}=${quoteIfNeeded(newVal)}`);
|
|
53
|
-
remaining.delete(line.key);
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
out.push(line.raw);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (!existing) {
|
|
60
|
-
out.length = 0;
|
|
61
|
-
out.push(HEADER.trimEnd());
|
|
62
|
-
out.push("");
|
|
63
|
-
}
|
|
64
|
-
while (out.length > 0 && out[out.length - 1] === "")
|
|
65
|
-
out.pop();
|
|
66
|
-
if (remaining.size > 0) {
|
|
67
|
-
if (out.length > 0)
|
|
68
|
-
out.push("");
|
|
69
|
-
for (const [k, v] of remaining)
|
|
70
|
-
out.push(`${k}=${quoteIfNeeded(v)}`);
|
|
71
|
-
}
|
|
72
|
-
return out.join("\n") + "\n";
|
|
73
|
-
}
|
|
74
|
-
export function writeConf(path, updates) {
|
|
75
|
-
const { text } = readExisting(path);
|
|
76
|
-
const merged = mergeConf(text, updates);
|
|
77
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
78
|
-
writeFileSync(path, merged, { encoding: "utf8" });
|
|
79
|
-
try {
|
|
80
|
-
chmodSync(path, 0o600);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
// chmod isn't meaningful on Windows; ignore.
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
//# sourceMappingURL=conf-writer.js.map
|
|
3
|
+
`;function f(n){return/[\s#'"]/.test(n)?`"${n.replace(/"/g,'\\"')}"`:n}function x(n,r){const s=c(n),t=new Map;for(const[e,o]of Object.entries(r))o!==void 0&&t.set(e,o);const i=[];for(const e of s)if(e.key&&t.has(e.key)){const o=t.get(e.key);i.push(`${e.key}=${f(o)}`),t.delete(e.key)}else i.push(e.raw);for(n||(i.length=0,i.push(k.trimEnd()),i.push(""));i.length>0&&i[i.length-1]==="";)i.pop();if(t.size>0){i.length>0&&i.push("");for(const[e,o]of t)i.push(`${e}=${f(o)}`)}return i.join(`
|
|
4
|
+
`)+`
|
|
5
|
+
`}function W(n,r){const{text:s}=y(n),t=x(s,r);g(m(n),{recursive:!0}),p(n,t,{encoding:"utf8"});try{d(n,384)}catch{}}export{S as PRIMARY_CONF_PATH,x as mergeConf,y as readExisting,W as writeConf};
|
|
@@ -1,44 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
export function scanDownloadsForGoogleCredentials(maxAgeMs = 60 * 60 * 1000) {
|
|
5
|
-
const dir = resolve(homedir(), "Downloads");
|
|
6
|
-
let entries;
|
|
7
|
-
try {
|
|
8
|
-
entries = readdirSync(dir);
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
const now = Date.now();
|
|
14
|
-
let best;
|
|
15
|
-
for (const name of entries) {
|
|
16
|
-
if (!name.startsWith("client_secret_") || !name.endsWith(".json"))
|
|
17
|
-
continue;
|
|
18
|
-
const full = resolve(dir, name);
|
|
19
|
-
let mtimeMs;
|
|
20
|
-
try {
|
|
21
|
-
mtimeMs = statSync(full).mtimeMs;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
const age = now - mtimeMs;
|
|
27
|
-
if (age < 0 || age > maxAgeMs)
|
|
28
|
-
continue;
|
|
29
|
-
if (!best || age < best.ageMs)
|
|
30
|
-
best = { path: full, ageMs: age };
|
|
31
|
-
}
|
|
32
|
-
return best;
|
|
33
|
-
}
|
|
34
|
-
export function formatAge(ms) {
|
|
35
|
-
const sec = Math.floor(ms / 1000);
|
|
36
|
-
if (sec < 60)
|
|
37
|
-
return `${sec}s ago`;
|
|
38
|
-
const min = Math.floor(sec / 60);
|
|
39
|
-
if (min < 60)
|
|
40
|
-
return `${min} min ago`;
|
|
41
|
-
const hr = Math.floor(min / 60);
|
|
42
|
-
return `${hr}h ago`;
|
|
43
|
-
}
|
|
44
|
-
//# sourceMappingURL=downloads-scan.js.map
|
|
1
|
+
import{readdirSync as m,statSync as d}from"node:fs";import{homedir as l}from"node:os";import{resolve as f}from"node:path";function p(r=3600*1e3){const t=f(l(),"Downloads");let n;try{n=m(t)}catch{return}const s=Date.now();let o;for(const i of n){if(!i.startsWith("client_secret_")||!i.endsWith(".json"))continue;const a=f(t,i);let c;try{c=d(a).mtimeMs}catch{continue}const e=s-c;e<0||e>r||(!o||e<o.ageMs)&&(o={path:a,ageMs:e})}return o}function M(r){const t=Math.floor(r/1e3);if(t<60)return`${t}s ago`;const n=Math.floor(t/60);return n<60?`${n} min ago`:`${Math.floor(n/60)}h ago`}export{M as formatAge,p as scanDownloadsForGoogleCredentials};
|
package/dist/setup/prompts.js
CHANGED
|
@@ -1,123 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
finally {
|
|
17
|
-
rl.close();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
export async function confirm(label, defaultYes = false) {
|
|
21
|
-
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
22
|
-
for (;;) {
|
|
23
|
-
const reply = (await ask(`${label} ${hint}`)).toLowerCase();
|
|
24
|
-
if (!reply)
|
|
25
|
-
return defaultYes;
|
|
26
|
-
if (reply.startsWith("y"))
|
|
27
|
-
return true;
|
|
28
|
-
if (reply.startsWith("n"))
|
|
29
|
-
return false;
|
|
30
|
-
process.stdout.write(" Please answer y or n.\n");
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
export async function pickFromList(label, items, render) {
|
|
34
|
-
if (items.length === 0)
|
|
35
|
-
return undefined;
|
|
36
|
-
process.stdout.write(`\n ${label}\n\n`);
|
|
37
|
-
items.forEach((item, idx) => {
|
|
38
|
-
process.stdout.write(` ${idx + 1}. ${render(item)}\n`);
|
|
39
|
-
});
|
|
40
|
-
process.stdout.write(" s. Skip\n\n");
|
|
41
|
-
for (;;) {
|
|
42
|
-
const reply = (await ask("Choice")).toLowerCase();
|
|
43
|
-
if (reply === "s" || reply === "")
|
|
44
|
-
return undefined;
|
|
45
|
-
const n = Number.parseInt(reply, 10);
|
|
46
|
-
if (Number.isInteger(n) && n >= 1 && n <= items.length)
|
|
47
|
-
return items[n - 1];
|
|
48
|
-
process.stdout.write(" Enter a number from the list, or 's' to skip.\n");
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export async function askSecret(label) {
|
|
52
|
-
process.stdout.write(` ${label}: `);
|
|
53
|
-
if (!isTTY()) {
|
|
54
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
55
|
-
try {
|
|
56
|
-
const line = await new Promise((resolve) => {
|
|
57
|
-
rl.question("", resolve);
|
|
58
|
-
});
|
|
59
|
-
return line.trim();
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
rl.close();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const stdin = process.stdin;
|
|
66
|
-
const wasRaw = stdin.isRaw;
|
|
67
|
-
stdin.setRawMode(true);
|
|
68
|
-
stdin.resume();
|
|
69
|
-
stdin.setEncoding("utf8");
|
|
70
|
-
let buf = "";
|
|
71
|
-
try {
|
|
72
|
-
for await (const chunk of stdin) {
|
|
73
|
-
let done = false;
|
|
74
|
-
for (const ch of chunk) {
|
|
75
|
-
const code = ch.charCodeAt(0);
|
|
76
|
-
if (ch === "\r" || ch === "\n") {
|
|
77
|
-
done = true;
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
if (code === 3) {
|
|
81
|
-
process.stdout.write("\n");
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
if (code === 127 || code === 8) {
|
|
85
|
-
buf = buf.slice(0, -1);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
buf += ch;
|
|
89
|
-
}
|
|
90
|
-
if (done)
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
stdin.setRawMode(wasRaw);
|
|
96
|
-
stdin.pause();
|
|
97
|
-
process.stdout.write("\n");
|
|
98
|
-
}
|
|
99
|
-
return buf.trim();
|
|
100
|
-
}
|
|
101
|
-
export function maskToken(t) {
|
|
102
|
-
if (t.length > 16)
|
|
103
|
-
return `${t.slice(0, 8)}...${t.slice(-4)}`;
|
|
104
|
-
return `${t.slice(0, 4)}...`;
|
|
105
|
-
}
|
|
106
|
-
export function openBrowser(url) {
|
|
107
|
-
const p = platform();
|
|
108
|
-
const cmd = p === "darwin" ? "open" : p === "win32" ? "start" : "xdg-open";
|
|
109
|
-
try {
|
|
110
|
-
const child = spawn(cmd, [url], { stdio: "ignore", detached: true });
|
|
111
|
-
child.on("error", () => {
|
|
112
|
-
process.stdout.write(` Open this URL: ${url}\n`);
|
|
113
|
-
});
|
|
114
|
-
child.unref();
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
process.stdout.write(` Open this URL: ${url}\n`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
export async function pause(label = "Press Enter to continue...") {
|
|
121
|
-
await ask(label);
|
|
122
|
-
}
|
|
123
|
-
//# sourceMappingURL=prompts.js.map
|
|
1
|
+
import{spawn as p}from"node:child_process";import{createInterface as a}from"node:readline";import{platform as f}from"node:os";function d(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}async function u(e,t=""){const n=t?` [${t}]`:"",s=a({input:process.stdin,output:process.stdout});try{return(await new Promise(i=>{s.question(` ${e}${n}: `,i)})).trim()||t}finally{s.close()}}async function g(e,t=!1){const n=t?"[Y/n]":"[y/N]";for(;;){const s=(await u(`${e} ${n}`)).toLowerCase();if(!s)return t;if(s.startsWith("y"))return!0;if(s.startsWith("n"))return!1;process.stdout.write(` Please answer y or n.
|
|
2
|
+
`)}}async function y(e,t,n){if(t.length!==0)for(process.stdout.write(`
|
|
3
|
+
${e}
|
|
4
|
+
|
|
5
|
+
`),t.forEach((s,r)=>{process.stdout.write(` ${r+1}. ${n(s)}
|
|
6
|
+
`)}),process.stdout.write(` s. Skip
|
|
7
|
+
|
|
8
|
+
`);;){const s=(await u("Choice")).toLowerCase();if(s==="s"||s==="")return;const r=Number.parseInt(s,10);if(Number.isInteger(r)&&r>=1&&r<=t.length)return t[r-1];process.stdout.write(` Enter a number from the list, or 's' to skip.
|
|
9
|
+
`)}}async function h(e){if(process.stdout.write(` ${e}: `),!d()){const r=a({input:process.stdin,output:process.stdout,terminal:!1});try{return(await new Promise(o=>{r.question("",o)})).trim()}finally{r.close()}}const t=process.stdin,n=t.isRaw;t.setRawMode(!0),t.resume(),t.setEncoding("utf8");let s="";try{for await(const r of t){let i=!1;for(const o of r){const c=o.charCodeAt(0);if(o==="\r"||o===`
|
|
10
|
+
`){i=!0;break}if(c===3&&(process.stdout.write(`
|
|
11
|
+
`),process.exit(1)),c===127||c===8){s=s.slice(0,-1);continue}s+=o}if(i)break}}finally{t.setRawMode(n),t.pause(),process.stdout.write(`
|
|
12
|
+
`)}return s.trim()}function $(e){return e.length>16?`${e.slice(0,8)}...${e.slice(-4)}`:`${e.slice(0,4)}...`}function T(e){const t=f(),n=t==="darwin"?"open":t==="win32"?"start":"xdg-open";try{const s=p(n,[e],{stdio:"ignore",detached:!0});s.on("error",()=>{process.stdout.write(` Open this URL: ${e}
|
|
13
|
+
`)}),s.unref()}catch{process.stdout.write(` Open this URL: ${e}
|
|
14
|
+
`)}}async function x(e="Press Enter to continue..."){await u(e)}export{u as ask,h as askSecret,g as confirm,$ as maskToken,T as openBrowser,x as pause,y as pickFromList};
|