depth-first-thinking 2.0.7 → 2.0.9

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,111 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import type { Node } from "./types";
3
+
4
+ export function addChildNode(parent: Node, title: string): Node {
5
+ const newNode = {
6
+ id: uuidv4(),
7
+ title: title.trim(),
8
+ status: "open" as const,
9
+ children: [],
10
+ created_at: new Date().toISOString(),
11
+ completed_at: null,
12
+ };
13
+ parent.children.push(newNode);
14
+ return newNode;
15
+ }
16
+
17
+ export function editNodeTitle(node: Node, newTitle: string): void {
18
+ node.title = newTitle.trim();
19
+ }
20
+
21
+ export function markNodeDone(node: Node): void {
22
+ node.status = "done";
23
+ node.completed_at = new Date().toISOString();
24
+ for (const child of node.children) {
25
+ markNodeDone(child);
26
+ }
27
+ }
28
+
29
+ export function markNodeOpen(node: Node): void {
30
+ node.status = "open";
31
+ node.completed_at = null;
32
+ }
33
+
34
+ export function toggleNodeStatus(node: Node): void {
35
+ if (node.status === "open") {
36
+ markNodeDone(node);
37
+ } else {
38
+ markNodeOpen(node);
39
+ }
40
+ }
41
+
42
+ export function deleteNode(parent: Node, nodeId: string): boolean {
43
+ const index = parent.children.findIndex((c) => c.id === nodeId);
44
+ if (index === -1) return false;
45
+ parent.children.splice(index, 1);
46
+ return true;
47
+ }
48
+
49
+ export function findNode(root: Node, id: string): Node | null {
50
+ if (root.id === id) return root;
51
+ for (const child of root.children) {
52
+ const found = findNode(child, id);
53
+ if (found) return found;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ export function findParent(root: Node, childId: string): Node | null {
59
+ for (const child of root.children) {
60
+ if (child.id === childId) return root;
61
+ const found = findParent(child, childId);
62
+ if (found) return found;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ export function buildPathToNode(root: Node, targetId: string): string[] | null {
68
+ if (root.id === targetId) return [root.id];
69
+ for (const child of root.children) {
70
+ const path = buildPathToNode(child, targetId);
71
+ if (path) return [root.id, ...path];
72
+ }
73
+ return null;
74
+ }
75
+
76
+ export function getNodePath(root: Node, targetId: string): Node[] | null {
77
+ if (root.id === targetId) return [root];
78
+ for (const child of root.children) {
79
+ const path = getNodePath(child, targetId);
80
+ if (path) return [root, ...path];
81
+ }
82
+ return null;
83
+ }
84
+
85
+ export function getSiblings(parent: Node): Node[] {
86
+ return parent.children;
87
+ }
88
+
89
+ export function getSiblingIndex(siblings: Node[], nodeId: string): number {
90
+ return siblings.findIndex((s) => s.id === nodeId);
91
+ }
92
+
93
+ export function getPreviousSibling(siblings: Node[], nodeId: string): Node | null {
94
+ const index = getSiblingIndex(siblings, nodeId);
95
+ if (index <= 0) return null;
96
+ return siblings[index - 1];
97
+ }
98
+
99
+ export function getNextSibling(siblings: Node[], nodeId: string): Node | null {
100
+ const index = getSiblingIndex(siblings, nodeId);
101
+ if (index === -1 || index >= siblings.length - 1) return null;
102
+ return siblings[index + 1];
103
+ }
104
+
105
+ export function countDescendants(node: Node): number {
106
+ let count = node.children.length;
107
+ for (const child of node.children) {
108
+ count += countDescendants(child);
109
+ }
110
+ return count;
111
+ }
@@ -0,0 +1,227 @@
1
+ import { mkdir, readdir, rename, rm } from "node:fs/promises";
2
+ import { getProjectPath, getProjectsDir } from "../utils/platform";
3
+ import type { Node, Project } from "./types";
4
+
5
+ function validateNode(node: unknown, path = "root"): node is Node {
6
+ if (!node || typeof node !== "object") {
7
+ throw new Error(`Invalid node at ${path}: expected object`);
8
+ }
9
+
10
+ const n = node as Record<string, unknown>;
11
+
12
+ if (typeof n.id !== "string" || n.id.length !== 36) {
13
+ throw new Error(`Invalid node at ${path}: id must be a 36-character UUID`);
14
+ }
15
+
16
+ if (typeof n.title !== "string" || n.title.length < 1 || n.title.length > 200) {
17
+ throw new Error(`Invalid node at ${path}: title must be 1-200 characters`);
18
+ }
19
+
20
+ if (n.status !== "open" && n.status !== "done") {
21
+ throw new Error(`Invalid node at ${path}: status must be "open" or "done"`);
22
+ }
23
+
24
+ if (!Array.isArray(n.children)) {
25
+ throw new Error(`Invalid node at ${path}: children must be an array`);
26
+ }
27
+
28
+ if (typeof n.created_at !== "string") {
29
+ throw new Error(`Invalid node at ${path}: created_at must be a string`);
30
+ }
31
+
32
+ if (n.completed_at !== null && typeof n.completed_at !== "string") {
33
+ throw new Error(`Invalid node at ${path}: completed_at must be string or null`);
34
+ }
35
+
36
+ for (let i = 0; i < n.children.length; i++) {
37
+ validateNode(n.children[i], `${path}.children[${i}]`);
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ function validateProjectSchema(project: unknown): project is Project {
44
+ if (!project || typeof project !== "object") {
45
+ throw new Error("Project must be an object");
46
+ }
47
+
48
+ const p = project as Record<string, unknown>;
49
+
50
+ if (typeof p.project_name !== "string") {
51
+ throw new Error("Project must have a project_name string");
52
+ }
53
+
54
+ if (typeof p.version !== "string") {
55
+ throw new Error("Project must have a version string");
56
+ }
57
+
58
+ if (typeof p.created_at !== "string") {
59
+ throw new Error("Project must have a created_at timestamp");
60
+ }
61
+
62
+ if (typeof p.modified_at !== "string") {
63
+ throw new Error("Project must have a modified_at timestamp");
64
+ }
65
+
66
+ validateNode(p.root);
67
+
68
+ return true;
69
+ }
70
+
71
+ export async function ensureProjectsDir(): Promise<void> {
72
+ const dir = getProjectsDir();
73
+ await mkdir(dir, { recursive: true });
74
+ }
75
+
76
+ export async function projectExists(name: string): Promise<boolean> {
77
+ const path = getProjectPath(name);
78
+ const file = Bun.file(path);
79
+ return await file.exists();
80
+ }
81
+
82
+ export async function loadProject(name: string): Promise<Project> {
83
+ const path = getProjectPath(name);
84
+ const file = Bun.file(path);
85
+
86
+ if (!(await file.exists())) {
87
+ throw new Error(`Project '${name}' not found. Use 'dft list' to see available projects.`);
88
+ }
89
+
90
+ let project: unknown;
91
+ try {
92
+ project = await file.json();
93
+ } catch {
94
+ throw new Error(`Project '${name}' has invalid JSON. The file may be corrupted.`);
95
+ }
96
+
97
+ try {
98
+ validateProjectSchema(project);
99
+ } catch (error) {
100
+ const detail = error instanceof Error ? error.message : "Unknown error";
101
+ throw new Error(`Project '${name}' has invalid structure: ${detail}`);
102
+ }
103
+
104
+ return project as Project;
105
+ }
106
+
107
+ export async function saveProject(project: Project): Promise<void> {
108
+ await ensureProjectsDir();
109
+
110
+ project.modified_at = new Date().toISOString();
111
+ const json = JSON.stringify(project, null, 2);
112
+ const projectPath = getProjectPath(project.project_name);
113
+ const tempPath = `${projectPath}.tmp`;
114
+
115
+ try {
116
+ await Bun.write(tempPath, json);
117
+ await rename(tempPath, projectPath);
118
+ } catch (error) {
119
+ try {
120
+ await rm(tempPath, { force: true });
121
+ } catch {
122
+ // Ignore cleanup errors
123
+ }
124
+ throw new Error(`Cannot write to project directory. Check permissions. ${error}`);
125
+ }
126
+ }
127
+
128
+ export async function listProjects(): Promise<
129
+ Array<{ name: string; nodeCount: number; createdAt: string }>
130
+ > {
131
+ const dir = getProjectsDir();
132
+
133
+ let files: string[];
134
+ try {
135
+ files = await readdir(dir);
136
+ } catch {
137
+ return [];
138
+ }
139
+
140
+ const projects: Array<{
141
+ name: string;
142
+ nodeCount: number;
143
+ createdAt: string;
144
+ }> = [];
145
+
146
+ for (const file of files) {
147
+ if (!file.endsWith(".json")) continue;
148
+
149
+ const name = file.slice(0, -5);
150
+ try {
151
+ const project = await loadProject(name);
152
+ const countNodes = (node: Node): number => {
153
+ let count = 1;
154
+ for (const child of node.children) {
155
+ count += countNodes(child);
156
+ }
157
+ return count;
158
+ };
159
+ const nodeCount = countNodes(project.root);
160
+ projects.push({
161
+ name: project.project_name,
162
+ nodeCount,
163
+ createdAt: project.created_at,
164
+ });
165
+ } catch {
166
+ // Skip corrupted files
167
+ }
168
+ }
169
+
170
+ projects.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
171
+
172
+ return projects;
173
+ }
174
+
175
+ export async function getMostOpenedProject(): Promise<string | null> {
176
+ const dir = getProjectsDir();
177
+
178
+ let files: string[];
179
+ try {
180
+ files = await readdir(dir);
181
+ } catch {
182
+ return null;
183
+ }
184
+
185
+ let mostOpenedProject: { name: string; openCount: number } | null = null;
186
+
187
+ for (const file of files) {
188
+ if (!file.endsWith(".json")) continue;
189
+
190
+ const name = file.slice(0, -5);
191
+ try {
192
+ const project = await loadProject(name);
193
+ const openCount = project.open_count ?? 0;
194
+
195
+ if (openCount > 0) {
196
+ if (
197
+ !mostOpenedProject ||
198
+ openCount > mostOpenedProject.openCount ||
199
+ (openCount === mostOpenedProject.openCount &&
200
+ project.project_name < mostOpenedProject.name)
201
+ ) {
202
+ mostOpenedProject = {
203
+ name: project.project_name,
204
+ openCount,
205
+ };
206
+ }
207
+ }
208
+ } catch {}
209
+ }
210
+
211
+ return mostOpenedProject?.name ?? null;
212
+ }
213
+
214
+ export async function deleteProject(name: string): Promise<void> {
215
+ const path = getProjectPath(name);
216
+ const file = Bun.file(path);
217
+
218
+ if (!(await file.exists())) {
219
+ throw new Error(`Project '${name}' not found. Use 'dft list' to see available projects.`);
220
+ }
221
+
222
+ try {
223
+ await rm(path);
224
+ } catch {
225
+ throw new Error("Cannot delete project file. Check permissions.");
226
+ }
227
+ }
@@ -0,0 +1,48 @@
1
+ export type NodeStatus = "open" | "done";
2
+
3
+ export interface Node {
4
+ id: string;
5
+ title: string;
6
+ status: NodeStatus;
7
+ children: Node[];
8
+ created_at: string;
9
+ completed_at: string | null;
10
+ }
11
+
12
+ export interface Project {
13
+ project_name: string;
14
+ version: string;
15
+ created_at: string;
16
+ modified_at: string;
17
+ root: Node;
18
+ open_count?: number;
19
+ }
20
+
21
+ export type ModalType = "new" | "edit" | "delete" | "help";
22
+
23
+ export interface ModalState {
24
+ type: ModalType;
25
+ inputValue?: string;
26
+ errorMessage?: string;
27
+ selectedButton?: number;
28
+ }
29
+
30
+ export interface AppState {
31
+ project: Project;
32
+ navigationStack: string[];
33
+ selectedIndex: number;
34
+ modalState: ModalState | null;
35
+ feedbackMessage: string | null;
36
+ feedbackTimeout?: ReturnType<typeof setTimeout>;
37
+ }
38
+
39
+ export const ExitCodes = {
40
+ SUCCESS: 0,
41
+ INVALID_NAME: 1,
42
+ ALREADY_EXISTS: 2,
43
+ FILESYSTEM_ERROR: 3,
44
+ NOT_FOUND: 1,
45
+ CANCELLED: 2,
46
+ CORRUPTED: 2,
47
+ TUI_ERROR: 3,
48
+ } as const;
package/src/index.ts ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from "commander";
4
+ import { deleteCommand } from "./commands/delete";
5
+ import { listCommand } from "./commands/list";
6
+ import { newCommand } from "./commands/new";
7
+ import { openCommand } from "./commands/open";
8
+ import { treeCommand } from "./commands/tree";
9
+ import { updateCommand } from "./commands/update";
10
+ import { getMostOpenedProject } from "./data/storage";
11
+ import { isValidProjectName } from "./utils/validation";
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name("dft")
17
+ .description("Depth-First Thinking - Solve problems the depth-first way")
18
+ .version("1.0.0");
19
+
20
+ program
21
+ .command("new <project_name>")
22
+ .aliases(["create", "init", "add"])
23
+ .description("Create a new project")
24
+ .action(async (projectName: string) => {
25
+ await newCommand(projectName);
26
+ });
27
+
28
+ program
29
+ .command("list")
30
+ .aliases(["ls", "show", "projects"])
31
+ .description("List all projects sorted by creation date")
32
+ .action(async () => {
33
+ await listCommand();
34
+ });
35
+
36
+ program
37
+ .command("delete <project_name>")
38
+ .aliases(["rm", "remove"])
39
+ .description("Delete an existing project")
40
+ .option("-y, --yes", "Skip confirmation prompt")
41
+ .action(async (projectName: string, options: { yes?: boolean }) => {
42
+ await deleteCommand(projectName, options);
43
+ });
44
+
45
+ program
46
+ .command("open <project_name>")
47
+ .aliases(["use", "start", "run"])
48
+ .description("Launch the interactive TUI session")
49
+ .action(async (projectName: string) => {
50
+ await openCommand(projectName);
51
+ });
52
+
53
+ program
54
+ .command("tree <project_name>")
55
+ .aliases(["view"])
56
+ .description("Print the tree structure to stdout")
57
+ .option("--show-status", "Show status markers (default: true)")
58
+ .option("--no-status", "Hide status markers")
59
+ .action(async (projectName: string, options: { status?: boolean }) => {
60
+ await treeCommand(projectName, options);
61
+ });
62
+
63
+ program
64
+ .command("update")
65
+ .aliases(["upgrade", "check-update"])
66
+ .description("Check for updates to dft")
67
+ .action(async () => {
68
+ await updateCommand();
69
+ });
70
+
71
+ const knownCommands = [
72
+ "new",
73
+ "create",
74
+ "init",
75
+ "add",
76
+ "list",
77
+ "ls",
78
+ "show",
79
+ "projects",
80
+ "delete",
81
+ "rm",
82
+ "remove",
83
+ "open",
84
+ "use",
85
+ "start",
86
+ "run",
87
+ "tree",
88
+ "view",
89
+ "update",
90
+ "upgrade",
91
+ "check-update",
92
+ "help",
93
+ ];
94
+
95
+ async function main() {
96
+ const args = process.argv.slice(2);
97
+
98
+ if (args.length === 0) {
99
+ const mostOpenedProject = await getMostOpenedProject();
100
+ if (mostOpenedProject) {
101
+ await openCommand(mostOpenedProject);
102
+ return;
103
+ }
104
+ await listCommand();
105
+ return;
106
+ }
107
+
108
+ if (
109
+ args.length > 0 &&
110
+ !args[0].startsWith("-") &&
111
+ !knownCommands.includes(args[0]) &&
112
+ isValidProjectName(args[0])
113
+ ) {
114
+ process.argv.splice(2, 0, "open");
115
+ }
116
+
117
+ await program.parseAsync(process.argv);
118
+ }
119
+
120
+ main().catch((error) => {
121
+ console.error("Error:", error.message || error);
122
+ process.exit(1);
123
+ });