archive-labs 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/LICENSE +21 -0
- package/README.md +357 -0
- package/dist/cli.js +8525 -0
- package/dist/recentHistory.js +205 -0
- package/package.json +53 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
export const recentHistoryVersion = 1;
|
|
6
|
+
export const defaultRecentCommandLimit = 10;
|
|
7
|
+
export const maxRecentHistoryItems = 50;
|
|
8
|
+
export const launcherRecentLimit = 3;
|
|
9
|
+
export const historyDir = path.join(os.homedir(), ".archive-labs");
|
|
10
|
+
export const historyPath = path.join(historyDir, "history.json");
|
|
11
|
+
const fileExists = async (targetPath) => {
|
|
12
|
+
try {
|
|
13
|
+
await access(targetPath);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const normalizeTimestamp = (value) => {
|
|
21
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const parsed = Date.parse(value);
|
|
25
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
26
|
+
};
|
|
27
|
+
const normalizeCommandStatus = (value) => value === "error" ? "error" : "success";
|
|
28
|
+
const normalizeCommandSource = (value) => value === "launcher" ? "launcher" : "cli";
|
|
29
|
+
const normalizeCommandGroupValue = (value) => {
|
|
30
|
+
switch (value) {
|
|
31
|
+
case "setup":
|
|
32
|
+
case "system":
|
|
33
|
+
case "check":
|
|
34
|
+
case "impact":
|
|
35
|
+
case "intelligence":
|
|
36
|
+
case "release":
|
|
37
|
+
case "utility":
|
|
38
|
+
return value;
|
|
39
|
+
default:
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const sanitizeHistoryItem = (value) => {
|
|
44
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const candidate = value;
|
|
48
|
+
const argv = Array.isArray(candidate.argv)
|
|
49
|
+
? candidate.argv.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
50
|
+
: [];
|
|
51
|
+
const command = typeof candidate.command === "string" && candidate.command.trim() ? candidate.command.trim() : null;
|
|
52
|
+
const cwd = typeof candidate.cwd === "string" && candidate.cwd.trim() ? path.resolve(candidate.cwd) : null;
|
|
53
|
+
const id = typeof candidate.id === "string" && candidate.id.trim() ? candidate.id.trim() : null;
|
|
54
|
+
const timestamp = normalizeTimestamp(candidate.timestamp);
|
|
55
|
+
if (!id || !command || !cwd || !timestamp || argv.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
argv,
|
|
61
|
+
branch: typeof candidate.branch === "string" && candidate.branch.trim() ? candidate.branch.trim() : null,
|
|
62
|
+
command,
|
|
63
|
+
...(normalizeCommandGroupValue(candidate.commandGroup) ? { commandGroup: normalizeCommandGroupValue(candidate.commandGroup) } : {}),
|
|
64
|
+
cwd,
|
|
65
|
+
durationMs: typeof candidate.durationMs === "number" && Number.isFinite(candidate.durationMs)
|
|
66
|
+
? Math.max(0, Math.round(candidate.durationMs))
|
|
67
|
+
: 0,
|
|
68
|
+
exitCode: typeof candidate.exitCode === "number" && Number.isFinite(candidate.exitCode)
|
|
69
|
+
? Math.round(candidate.exitCode)
|
|
70
|
+
: 0,
|
|
71
|
+
repository: typeof candidate.repository === "string" && candidate.repository.trim() ? candidate.repository.trim() : null,
|
|
72
|
+
rerunnable: candidate.rerunnable !== false,
|
|
73
|
+
source: normalizeCommandSource(candidate.source),
|
|
74
|
+
status: normalizeCommandStatus(candidate.status),
|
|
75
|
+
timestamp,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const quoteShellArg = (value) => (/[\s"]/u.test(value) ? JSON.stringify(value) : value);
|
|
79
|
+
const normalizeRepository = (value) => (value?.trim().toLowerCase() ? value.trim().toLowerCase() : null);
|
|
80
|
+
const compareRecency = (left, right) => (Date.parse(right.timestamp) || 0) - (Date.parse(left.timestamp) || 0);
|
|
81
|
+
export const normalizeHistoryCwd = (cwd) => path.resolve(cwd);
|
|
82
|
+
export const inferCommandGroup = (argv) => {
|
|
83
|
+
const [head = "", next = ""] = argv.map((entry) => entry.trim().toLowerCase());
|
|
84
|
+
if (head === "init" || head === "sync") {
|
|
85
|
+
return "setup";
|
|
86
|
+
}
|
|
87
|
+
if (head === "status" || head === "events") {
|
|
88
|
+
return "system";
|
|
89
|
+
}
|
|
90
|
+
if (head === "check") {
|
|
91
|
+
return "check";
|
|
92
|
+
}
|
|
93
|
+
if (head === "impact") {
|
|
94
|
+
return "impact";
|
|
95
|
+
}
|
|
96
|
+
if (head === "ask" || head === "why") {
|
|
97
|
+
return "intelligence";
|
|
98
|
+
}
|
|
99
|
+
if (head === "release") {
|
|
100
|
+
return "release";
|
|
101
|
+
}
|
|
102
|
+
if (head === "login" || head === "doctor" || head === "ping" || head === "help" || head === "version" || head === "recent") {
|
|
103
|
+
return "utility";
|
|
104
|
+
}
|
|
105
|
+
if (!head && next) {
|
|
106
|
+
return inferCommandGroup([next]);
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
export const shouldSkipHistoryArgv = (argv) => {
|
|
111
|
+
const tokens = argv
|
|
112
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
if (tokens.length === 0) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
const [head] = tokens;
|
|
118
|
+
if (head === "recent" || head === "help" || head === "version") {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return tokens.some((token) => /^__archive_internal/i.test(token) ||
|
|
122
|
+
/\/auth\/callback/i.test(token) ||
|
|
123
|
+
/\bcli_(state|port)\b/i.test(token));
|
|
124
|
+
};
|
|
125
|
+
export const formatHistoryCommand = (argv) => ["archive", ...argv].map(quoteShellArg).join(" ");
|
|
126
|
+
export const createRecentHistoryItem = (input) => {
|
|
127
|
+
const argv = input.argv.map((entry) => String(entry));
|
|
128
|
+
const commandGroup = inferCommandGroup(argv);
|
|
129
|
+
return {
|
|
130
|
+
id: crypto.randomUUID(),
|
|
131
|
+
argv,
|
|
132
|
+
branch: input.branch?.trim() ? input.branch.trim() : null,
|
|
133
|
+
command: formatHistoryCommand(argv),
|
|
134
|
+
...(commandGroup ? { commandGroup } : {}),
|
|
135
|
+
cwd: normalizeHistoryCwd(input.cwd),
|
|
136
|
+
durationMs: typeof input.durationMs === "number" && Number.isFinite(input.durationMs)
|
|
137
|
+
? Math.max(0, Math.round(input.durationMs))
|
|
138
|
+
: 0,
|
|
139
|
+
exitCode: typeof input.exitCode === "number" && Number.isFinite(input.exitCode) ? Math.round(input.exitCode) : 0,
|
|
140
|
+
repository: input.repository?.trim() ? input.repository.trim() : null,
|
|
141
|
+
rerunnable: input.rerunnable !== false,
|
|
142
|
+
source: input.source ?? "cli",
|
|
143
|
+
status: input.status ?? "success",
|
|
144
|
+
timestamp: normalizeTimestamp(input.timestamp) ?? new Date().toISOString(),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
export const loadRecentHistory = async (targetPath = historyPath) => {
|
|
148
|
+
if (!(await fileExists(targetPath))) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const raw = await readFile(targetPath, "utf8");
|
|
153
|
+
const parsed = JSON.parse(raw);
|
|
154
|
+
const items = Array.isArray(parsed?.items)
|
|
155
|
+
? parsed.items.map(sanitizeHistoryItem).filter((item) => Boolean(item))
|
|
156
|
+
: [];
|
|
157
|
+
return items.sort(compareRecency);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
export const writeRecentHistory = async (items, targetPath = historyPath) => {
|
|
164
|
+
const normalizedItems = [...items].sort(compareRecency).slice(0, maxRecentHistoryItems);
|
|
165
|
+
await mkdir(path.dirname(targetPath), { mode: 0o700, recursive: true });
|
|
166
|
+
const payload = {
|
|
167
|
+
items: normalizedItems,
|
|
168
|
+
version: recentHistoryVersion,
|
|
169
|
+
};
|
|
170
|
+
await writeFile(targetPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
171
|
+
if (process.platform !== "win32") {
|
|
172
|
+
await Promise.allSettled([chmod(path.dirname(targetPath), 0o700), chmod(targetPath, 0o600)]);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
export const appendRecentHistory = async (item, targetPath = historyPath) => {
|
|
176
|
+
const existing = await loadRecentHistory(targetPath);
|
|
177
|
+
await writeRecentHistory([item, ...existing], targetPath);
|
|
178
|
+
};
|
|
179
|
+
export const buildLauncherRecentEntries = (items, context = {}, limit = launcherRecentLimit) => {
|
|
180
|
+
const normalizedCwd = context.cwd ? normalizeHistoryCwd(context.cwd) : null;
|
|
181
|
+
const normalizedRepository = normalizeRepository(context.repository);
|
|
182
|
+
return items
|
|
183
|
+
.filter((item) => item.rerunnable)
|
|
184
|
+
.map((item) => {
|
|
185
|
+
let rank = 0;
|
|
186
|
+
if (normalizedCwd && normalizeHistoryCwd(item.cwd) === normalizedCwd) {
|
|
187
|
+
rank = 2;
|
|
188
|
+
}
|
|
189
|
+
else if (normalizedRepository && normalizeRepository(item.repository) === normalizedRepository) {
|
|
190
|
+
rank = 1;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
item,
|
|
194
|
+
rank,
|
|
195
|
+
};
|
|
196
|
+
})
|
|
197
|
+
.sort((left, right) => {
|
|
198
|
+
if (right.rank !== left.rank) {
|
|
199
|
+
return right.rank - left.rank;
|
|
200
|
+
}
|
|
201
|
+
return compareRecency(left.item, right.item);
|
|
202
|
+
})
|
|
203
|
+
.slice(0, Math.max(0, Math.floor(limit)))
|
|
204
|
+
.map((entry) => entry.item);
|
|
205
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "archive-labs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Archive Labs CLI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"preferGlobal": true,
|
|
7
|
+
"packageManager": "pnpm@10.33.0",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"archive": "./dist/cli.js",
|
|
11
|
+
"archive-labs": "./dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.13.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"dev": "tsx watch src/cli.ts",
|
|
25
|
+
"start": "node dist/cli.js",
|
|
26
|
+
"smoke": "node dist/cli.js version && node dist/cli.js help",
|
|
27
|
+
"pack:verify": "node scripts/verify-pack.mjs",
|
|
28
|
+
"lint": "npm run typecheck",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"test:integration": "npm run build && node scripts/run-node-tests.mjs",
|
|
31
|
+
"test": "npm run test:integration",
|
|
32
|
+
"ci": "npm run clean && npm run typecheck && npm run build && npm run smoke && npm run test",
|
|
33
|
+
"prepack": "npm run build",
|
|
34
|
+
"prepublishOnly": "npm run ci"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"archive-labs",
|
|
38
|
+
"archive",
|
|
39
|
+
"cli"
|
|
40
|
+
],
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.16.5",
|
|
43
|
+
"tsx": "^4.20.3",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@clack/prompts": "^1.2.0",
|
|
48
|
+
"chalk": "^5.6.2",
|
|
49
|
+
"commander": "^14.0.3",
|
|
50
|
+
"listr2": "^10.2.1",
|
|
51
|
+
"ora": "^9.3.0"
|
|
52
|
+
}
|
|
53
|
+
}
|