@dreamlake/ml-dash 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.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * `ml-dash remove` — delete a project and everything under it.
3
+ *
4
+ * The confirmation is a typed project name, not a y/n: the action deletes
5
+ * every experiment, metric, log and file in the project and cannot be undone,
6
+ * so a mistyped `-p` should not be one keystroke away from destroying the
7
+ * wrong project. `-y` skips it for scripts.
8
+ *
9
+ * A project that does not exist exits 0 — "make sure this is gone" has already
10
+ * succeeded — while a permission failure exits 1.
11
+ *
12
+ * The confirmation is read from stdin whether or not it is a terminal, which
13
+ * is what `console.input()` did in Python: `echo my-project | ml-dash remove -p
14
+ * my-project` is a supported way to confirm. Refusing every non-tty invocation
15
+ * would have broken that. Reaching end-of-input without a matching line
16
+ * cancels and exits 0 rather than hanging on a prompt nobody can answer.
17
+ */
18
+ import { createInterface } from "node:readline/promises";
19
+ import { HttpError } from "../client.js";
20
+ import { makeClient, notAuthenticatedMessage, resolveContext } from "../cli/context.js";
21
+ import { splitProjectArg } from "./create.js";
22
+ import { bold, dim, green, red, yellow } from "../util/ansi.js";
23
+ export const spec = {
24
+ name: "remove",
25
+ help: "Delete a project",
26
+ description: `Delete a project from ml-dash.
27
+
28
+ WARNING: This will delete the project and all its experiments, metrics, files, and logs.
29
+ This action cannot be undone.
30
+
31
+ Examples:
32
+ ml-dash remove -p my-project
33
+ ml-dash remove -p geyang/old-project
34
+ ml-dash remove -p my-project -y`,
35
+ options: [
36
+ { flags: ["-p", "--project"], dest: "project", required: true, metavar: "PROJECT", help: "Project name or namespace/project (e.g. 'my-project' or 'tom/my-project')" },
37
+ { flags: ["-y", "--yes"], dest: "yes", boolean: true, help: "Skip confirmation prompt" },
38
+ { flags: ["--dash-url", "--api-url"], dest: "dash_url", metavar: "URL", help: "ML-Dash server URL (default: https://api.dash.ml)" },
39
+ ],
40
+ };
41
+ /** The typed confirmation, or null when stdin ended without one. */
42
+ async function promptForName() {
43
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
44
+ try {
45
+ return await new Promise((resolve) => {
46
+ rl.question("Type the project name to confirm deletion: ").then((answer) => resolve(answer.trim()), () => resolve(null));
47
+ // A closed pipe never answers the question; without this the process
48
+ // would sit on a prompt that can no longer be typed into.
49
+ rl.once("close", () => resolve(null));
50
+ });
51
+ }
52
+ finally {
53
+ rl.close();
54
+ }
55
+ }
56
+ export async function run(args) {
57
+ const ctx = resolveContext(args);
58
+ const raw = String(args.project);
59
+ const split = splitProjectArg(raw);
60
+ if (!split) {
61
+ console.error(`${red("Error:")} Project can have at most 2 parts (namespace/project).\n` +
62
+ `Got: ${raw}\n\nExamples:\n ml-dash remove -p my-project\n ml-dash remove -p geyang/old-project`);
63
+ return 1;
64
+ }
65
+ if (!ctx.apiKey) {
66
+ console.error(`${red("Error:")} ${notAuthenticatedMessage(ctx)}`);
67
+ return 1;
68
+ }
69
+ const client = makeClient(ctx, split.namespace);
70
+ try {
71
+ const namespace = await client.namespace();
72
+ if (!namespace) {
73
+ console.error(`${red("Error:")} Could not determine namespace. Please login first.`);
74
+ return 1;
75
+ }
76
+ const fullPath = `${namespace}/${split.project}`;
77
+ const projectId = await client.getProjectId(split.project);
78
+ if (!projectId) {
79
+ console.log(`${yellow("⚠")} Project '${bold(fullPath)}' not found.`);
80
+ return 0;
81
+ }
82
+ if (!args.yes) {
83
+ console.log(`\n${red(bold("⚠ WARNING ⚠"))}\n\n` +
84
+ `You are about to delete project: ${bold(fullPath)}\n` +
85
+ "This will permanently delete:\n" +
86
+ " • All experiments in this project\n" +
87
+ " • All metrics and logs\n" +
88
+ " • All uploaded files\n\n" +
89
+ `${red("This action CANNOT be undone.")}\n`);
90
+ const answer = await promptForName();
91
+ if (answer === null) {
92
+ console.log(`\n${yellow("Deletion cancelled.")} No confirmation on stdin — pass -y to confirm non-interactively.`);
93
+ return 0;
94
+ }
95
+ if (answer !== split.project) {
96
+ console.log(`\n${yellow("Deletion cancelled.")}`);
97
+ return 0;
98
+ }
99
+ }
100
+ console.log(`\n${dim(`Deleting project '${fullPath}'...`)}`);
101
+ const result = await client.deleteProject(split.project);
102
+ console.log(`${green("✓")} Project '${bold(split.project)}' deleted from namespace '${bold(namespace)}'`);
103
+ if (result?.deleted != null)
104
+ console.log(` Deleted nodes: ${result.deleted}`);
105
+ if (result?.experiments != null)
106
+ console.log(` Deleted experiments: ${result.experiments}`);
107
+ return 0;
108
+ }
109
+ catch (e) {
110
+ if (e instanceof HttpError) {
111
+ if (e.status === 404) {
112
+ console.log(`${yellow("⚠")} Project '${bold(split.project)}' not found.`);
113
+ return 0;
114
+ }
115
+ if (e.status === 403) {
116
+ console.error(`${red("Error:")} Permission denied.`);
117
+ return 1;
118
+ }
119
+ }
120
+ console.error(`${red("Error deleting project:")} ${e.message}`);
121
+ return 1;
122
+ }
123
+ }