@runloop/rl-cli 1.1.0 → 1.2.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 +1 -0
- package/dist/commands/blueprint/prune.js +270 -0
- package/dist/utils/commands.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ rli blueprint list # List all blueprints
|
|
|
106
106
|
rli blueprint create # Create a new blueprint
|
|
107
107
|
rli blueprint get <name-or-id> # Get blueprint details by name or ID (...
|
|
108
108
|
rli blueprint logs <name-or-id> # Get blueprint build logs by name or I...
|
|
109
|
+
rli blueprint prune <name> # Delete old blueprint builds, keeping ...
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
### Object Commands (alias: `obj`)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint prune command - Delete old blueprint builds
|
|
3
|
+
*/
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
import { getClient } from "../../utils/client.js";
|
|
6
|
+
import { output, outputError } from "../../utils/output.js";
|
|
7
|
+
/**
|
|
8
|
+
* Fetch all blueprints with a given name (handles pagination)
|
|
9
|
+
*/
|
|
10
|
+
async function fetchAllBlueprintsWithName(name) {
|
|
11
|
+
const client = getClient();
|
|
12
|
+
const allBlueprints = [];
|
|
13
|
+
let hasMore = true;
|
|
14
|
+
let startingAfter = undefined;
|
|
15
|
+
while (hasMore) {
|
|
16
|
+
const params = { name, limit: 100 };
|
|
17
|
+
if (startingAfter) {
|
|
18
|
+
params.starting_after = startingAfter;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const page = await client.blueprints.list(params);
|
|
22
|
+
const blueprints = (page.blueprints || []);
|
|
23
|
+
allBlueprints.push(...blueprints);
|
|
24
|
+
hasMore = page.has_more || false;
|
|
25
|
+
if (hasMore && blueprints.length > 0) {
|
|
26
|
+
startingAfter = blueprints[blueprints.length - 1].id;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
hasMore = false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error("Warning: Error fetching blueprints:", error);
|
|
34
|
+
// Continue with partial results
|
|
35
|
+
hasMore = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return allBlueprints;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Categorize blueprints into successful and failed, and determine what to keep/delete
|
|
42
|
+
*/
|
|
43
|
+
function categorizeBlueprints(blueprints, keepCount) {
|
|
44
|
+
// Filter successful builds
|
|
45
|
+
const successful = blueprints.filter((b) => b.status === "build_complete" || b.status === "building_complete");
|
|
46
|
+
// Filter failed builds
|
|
47
|
+
const failed = blueprints.filter((b) => b.status !== "build_complete" && b.status !== "building_complete");
|
|
48
|
+
// Sort successful by create_time_ms descending (newest first)
|
|
49
|
+
successful.sort((a, b) => (b.create_time_ms || 0) - (a.create_time_ms || 0));
|
|
50
|
+
// Determine what to keep and delete
|
|
51
|
+
const toKeep = successful.slice(0, keepCount);
|
|
52
|
+
const toDelete = [...successful.slice(keepCount), ...failed];
|
|
53
|
+
return {
|
|
54
|
+
toKeep,
|
|
55
|
+
toDelete,
|
|
56
|
+
successful,
|
|
57
|
+
failed,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Format a timestamp for display
|
|
62
|
+
*/
|
|
63
|
+
function formatTimestamp(createTimeMs) {
|
|
64
|
+
if (!createTimeMs) {
|
|
65
|
+
return "unknown time";
|
|
66
|
+
}
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const diffMs = now - createTimeMs;
|
|
69
|
+
const diffMinutes = Math.floor(diffMs / 60000);
|
|
70
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
71
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
72
|
+
if (diffMinutes < 60) {
|
|
73
|
+
return `${diffMinutes} minute${diffMinutes !== 1 ? "s" : ""} ago`;
|
|
74
|
+
}
|
|
75
|
+
else if (diffHours < 24) {
|
|
76
|
+
return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Display a summary of what will be kept and deleted
|
|
84
|
+
*/
|
|
85
|
+
function displaySummary(name, result, isDryRun) {
|
|
86
|
+
const total = result.successful.length + result.failed.length;
|
|
87
|
+
console.log(`\nAnalyzing blueprints named "${name}"...`);
|
|
88
|
+
console.log(`\nFound ${total} blueprint${total !== 1 ? "s" : ""}:`);
|
|
89
|
+
console.log(` ✓ ${result.successful.length} successful build${result.successful.length !== 1 ? "s" : ""}`);
|
|
90
|
+
console.log(` ✗ ${result.failed.length} failed build${result.failed.length !== 1 ? "s" : ""}`);
|
|
91
|
+
// Show what will be kept
|
|
92
|
+
console.log(`\nKeeping (${result.toKeep.length} most recent successful):`);
|
|
93
|
+
if (result.toKeep.length === 0) {
|
|
94
|
+
console.log(" (none - no successful builds found)");
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (const blueprint of result.toKeep) {
|
|
98
|
+
console.log(` ✓ ${blueprint.id} - Created ${formatTimestamp(blueprint.create_time_ms)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Show what will be deleted
|
|
102
|
+
console.log(`\n${isDryRun ? "Would delete" : "To be deleted"} (${result.toDelete.length} blueprint${result.toDelete.length !== 1 ? "s" : ""}):`);
|
|
103
|
+
if (result.toDelete.length === 0) {
|
|
104
|
+
console.log(" (none)");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Show all blueprints without summarizing
|
|
108
|
+
for (const blueprint of result.toDelete) {
|
|
109
|
+
const icon = blueprint.status === "build_complete" ||
|
|
110
|
+
blueprint.status === "building_complete"
|
|
111
|
+
? "✓"
|
|
112
|
+
: "✗";
|
|
113
|
+
const statusLabel = blueprint.status === "build_complete" ||
|
|
114
|
+
blueprint.status === "building_complete"
|
|
115
|
+
? "successful"
|
|
116
|
+
: "failed";
|
|
117
|
+
console.log(` ${icon} ${blueprint.id} - Created ${formatTimestamp(blueprint.create_time_ms)} (${statusLabel})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Display all deleted blueprints
|
|
123
|
+
*/
|
|
124
|
+
function displayDeletedBlueprints(deleted) {
|
|
125
|
+
if (deleted.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log("\nDeleted blueprints:");
|
|
129
|
+
for (const blueprint of deleted) {
|
|
130
|
+
const icon = blueprint.status === "build_complete" ||
|
|
131
|
+
blueprint.status === "building_complete"
|
|
132
|
+
? "✓"
|
|
133
|
+
: "✗";
|
|
134
|
+
const statusLabel = blueprint.status === "build_complete" ||
|
|
135
|
+
blueprint.status === "building_complete"
|
|
136
|
+
? "successful"
|
|
137
|
+
: "failed";
|
|
138
|
+
console.log(` ${icon} ${blueprint.id} - Created ${formatTimestamp(blueprint.create_time_ms)} (${statusLabel})`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Prompt user for confirmation
|
|
143
|
+
*/
|
|
144
|
+
async function confirmDeletion(count) {
|
|
145
|
+
const rl = readline.createInterface({
|
|
146
|
+
input: process.stdin,
|
|
147
|
+
output: process.stdout,
|
|
148
|
+
});
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
rl.question(`\nDelete ${count} blueprint${count !== 1 ? "s" : ""}? (y/N): `, (answer) => {
|
|
151
|
+
rl.close();
|
|
152
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Delete blueprints with error tracking
|
|
158
|
+
*/
|
|
159
|
+
async function deleteBlueprintsWithTracking(blueprints) {
|
|
160
|
+
const client = getClient();
|
|
161
|
+
const results = {
|
|
162
|
+
deleted: [],
|
|
163
|
+
failed: [],
|
|
164
|
+
};
|
|
165
|
+
for (const blueprint of blueprints) {
|
|
166
|
+
try {
|
|
167
|
+
await client.blueprints.delete(blueprint.id);
|
|
168
|
+
results.deleted.push(blueprint);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
results.failed.push({
|
|
172
|
+
id: blueprint.id,
|
|
173
|
+
error: error instanceof Error ? error.message : String(error),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Main prune function
|
|
181
|
+
*/
|
|
182
|
+
export async function pruneBlueprints(name, options = {}) {
|
|
183
|
+
try {
|
|
184
|
+
// Parse and validate options
|
|
185
|
+
const isDryRun = options.dryRun || false;
|
|
186
|
+
const autoConfirm = options.yes || false;
|
|
187
|
+
const keepCount = parseInt(options.keep || "1", 10);
|
|
188
|
+
if (isNaN(keepCount) || keepCount < 1) {
|
|
189
|
+
outputError("--keep must be a positive integer");
|
|
190
|
+
}
|
|
191
|
+
// Fetch all blueprints with the given name
|
|
192
|
+
console.log(`Fetching blueprints named "${name}"...`);
|
|
193
|
+
const blueprints = await fetchAllBlueprintsWithName(name);
|
|
194
|
+
// Handle no blueprints found
|
|
195
|
+
if (blueprints.length === 0) {
|
|
196
|
+
console.log(`No blueprints found with name: ${name}`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Categorize blueprints
|
|
200
|
+
const categorized = categorizeBlueprints(blueprints, keepCount);
|
|
201
|
+
// Display summary
|
|
202
|
+
displaySummary(name, categorized, isDryRun);
|
|
203
|
+
// Handle dry-run mode
|
|
204
|
+
if (isDryRun) {
|
|
205
|
+
console.log("\n(Dry run - no changes made)");
|
|
206
|
+
const result = {
|
|
207
|
+
blueprintName: name,
|
|
208
|
+
totalFound: blueprints.length,
|
|
209
|
+
successfulBuilds: categorized.successful.length,
|
|
210
|
+
failedBuilds: categorized.failed.length,
|
|
211
|
+
kept: categorized.toKeep,
|
|
212
|
+
deleted: [],
|
|
213
|
+
failed: [],
|
|
214
|
+
dryRun: true,
|
|
215
|
+
};
|
|
216
|
+
if (options.output && options.output !== "text") {
|
|
217
|
+
output(result, { format: options.output, defaultFormat: "json" });
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Handle nothing to delete
|
|
222
|
+
if (categorized.toDelete.length === 0) {
|
|
223
|
+
console.log("\nNothing to delete.");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// Warn if no successful builds
|
|
227
|
+
if (categorized.successful.length === 0) {
|
|
228
|
+
console.log("\nWarning: No successful builds found. Only deleting failed builds.");
|
|
229
|
+
}
|
|
230
|
+
// Get confirmation unless --yes flag is set
|
|
231
|
+
if (!autoConfirm) {
|
|
232
|
+
const confirmed = await confirmDeletion(categorized.toDelete.length);
|
|
233
|
+
if (!confirmed) {
|
|
234
|
+
console.log("\nOperation cancelled.");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Perform deletions
|
|
239
|
+
console.log(`\nDeleting ${categorized.toDelete.length} blueprint${categorized.toDelete.length !== 1 ? "s" : ""}...`);
|
|
240
|
+
const deletionResults = await deleteBlueprintsWithTracking(categorized.toDelete);
|
|
241
|
+
// Display results
|
|
242
|
+
console.log("\nResults:");
|
|
243
|
+
console.log(` ✓ Successfully deleted: ${deletionResults.deleted.length} blueprint${deletionResults.deleted.length !== 1 ? "s" : ""}`);
|
|
244
|
+
// Show all deleted blueprints
|
|
245
|
+
displayDeletedBlueprints(deletionResults.deleted);
|
|
246
|
+
if (deletionResults.failed.length > 0) {
|
|
247
|
+
console.log(`\n ✗ Failed to delete: ${deletionResults.failed.length} blueprint${deletionResults.failed.length !== 1 ? "s" : ""}`);
|
|
248
|
+
for (const failure of deletionResults.failed) {
|
|
249
|
+
console.log(` - ${failure.id}: ${failure.error}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Output structured data if requested
|
|
253
|
+
if (options.output && options.output !== "text") {
|
|
254
|
+
const result = {
|
|
255
|
+
blueprintName: name,
|
|
256
|
+
totalFound: blueprints.length,
|
|
257
|
+
successfulBuilds: categorized.successful.length,
|
|
258
|
+
failedBuilds: categorized.failed.length,
|
|
259
|
+
kept: categorized.toKeep,
|
|
260
|
+
deleted: deletionResults.deleted,
|
|
261
|
+
failed: deletionResults.failed,
|
|
262
|
+
dryRun: false,
|
|
263
|
+
};
|
|
264
|
+
output(result, { format: options.output, defaultFormat: "json" });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
outputError("Failed to prune blueprints", error);
|
|
269
|
+
}
|
|
270
|
+
}
|
package/dist/utils/commands.js
CHANGED
|
@@ -300,6 +300,17 @@ export function createProgram() {
|
|
|
300
300
|
const { getBlueprintLogs } = await import("../commands/blueprint/logs.js");
|
|
301
301
|
await getBlueprintLogs({ id, ...options });
|
|
302
302
|
});
|
|
303
|
+
blueprint
|
|
304
|
+
.command("prune <name>")
|
|
305
|
+
.description("Delete old blueprint builds, keeping only recent successful ones")
|
|
306
|
+
.option("--dry-run", "Show what would be deleted without actually deleting")
|
|
307
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
308
|
+
.option("--keep <n>", "Number of successful builds to keep", "1")
|
|
309
|
+
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
310
|
+
.action(async (name, options) => {
|
|
311
|
+
const { pruneBlueprints } = await import("../commands/blueprint/prune.js");
|
|
312
|
+
await pruneBlueprints(name, options);
|
|
313
|
+
});
|
|
303
314
|
// Object storage commands
|
|
304
315
|
const object = program
|
|
305
316
|
.command("object")
|