totopo 0.5.0 → 0.6.0-rc-2
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/package.json +1 -1
- package/src/core/commands/dev.ts +142 -9
package/package.json
CHANGED
package/src/core/commands/dev.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// =========================================================================================================================================
|
|
6
6
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { basename, join } from "node:path";
|
|
11
|
-
import { cancel, isCancel, log, multiselect, outro, select } from "@clack/prompts";
|
|
11
|
+
import { cancel, groupMultiselect, isCancel, log, multiselect, outro, path, select } from "@clack/prompts";
|
|
12
12
|
|
|
13
13
|
// biome-ignore lint/style/noNonNullAssertion: guarded immediately below; non-null assertion needed for closure type inference
|
|
14
14
|
const workspaceDir = process.env.TOTOPO_REPO_ROOT!;
|
|
@@ -66,11 +66,60 @@ async function promptScope(): Promise<ScopeConfig> {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// ─── Prompt: selective path selection ─────────────────────────────────────────
|
|
69
|
+
// Recursively expands a selected path into its children when a nested exclusion target is found,
|
|
70
|
+
// until the excluded path itself can be dropped from the list.
|
|
71
|
+
function expandExclusion(paths: string[], excl: string): string[] {
|
|
72
|
+
if (paths.includes(excl)) {
|
|
73
|
+
return paths.filter((p) => p !== excl);
|
|
74
|
+
}
|
|
75
|
+
const ancestor = paths.find((p) => excl.startsWith(`${p}/`));
|
|
76
|
+
if (!ancestor) {
|
|
77
|
+
log.warn(`Cannot exclude "${excl}" — it is not within any selected path. Skipping.`);
|
|
78
|
+
return paths;
|
|
79
|
+
}
|
|
80
|
+
const children = readdirSync(join(cwd, ancestor)).map((child) => `${ancestor}/${child}`);
|
|
81
|
+
const withoutAncestor = paths.filter((p) => p !== ancestor);
|
|
82
|
+
return expandExclusion([...withoutAncestor, ...children], excl);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scanCwdDepth2(): { dirs: Record<string, string[]>; files: string[] } {
|
|
86
|
+
const dirs: Record<string, string[]> = {};
|
|
87
|
+
const files: string[] = [];
|
|
88
|
+
|
|
89
|
+
for (const item of readdirSync(cwd)) {
|
|
90
|
+
const itemPath = join(cwd, item);
|
|
91
|
+
if (statSync(itemPath).isDirectory()) {
|
|
92
|
+
const children = readdirSync(itemPath).map((child) => `${item}/${child}`);
|
|
93
|
+
if (children.length === 0) {
|
|
94
|
+
files.push(item); // empty dir → treat as flat item
|
|
95
|
+
} else {
|
|
96
|
+
dirs[item] = children;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
files.push(item);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { dirs, files };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeSelection(selected: string[], dirNames: string[]): string[] {
|
|
107
|
+
const withoutGroupKey = selected.filter((s) => s !== "Files");
|
|
108
|
+
const dirSet = new Set(dirNames);
|
|
109
|
+
|
|
110
|
+
return withoutGroupKey.filter((s) => {
|
|
111
|
+
const slashIdx = s.indexOf("/");
|
|
112
|
+
if (slashIdx === -1) return true; // depth-1 item, always keep
|
|
113
|
+
const parent = s.slice(0, slashIdx);
|
|
114
|
+
return !(dirSet.has(parent) && withoutGroupKey.includes(parent));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
69
118
|
async function promptSelectivePaths(): Promise<string[]> {
|
|
70
|
-
const allItems = readdirSync(cwd)
|
|
119
|
+
const allItems = readdirSync(cwd);
|
|
71
120
|
|
|
72
121
|
if (allItems.length === 0) {
|
|
73
|
-
log.warn("No
|
|
122
|
+
log.warn("No files/folders in current directory — falling back to cwd mode.");
|
|
74
123
|
return [];
|
|
75
124
|
}
|
|
76
125
|
|
|
@@ -88,21 +137,105 @@ async function promptSelectivePaths(): Promise<string[]> {
|
|
|
88
137
|
}
|
|
89
138
|
|
|
90
139
|
const style = styleChoice as "only" | "except";
|
|
91
|
-
const
|
|
140
|
+
const { dirs, files } = scanCwdDepth2();
|
|
141
|
+
const dirNames = Object.keys(dirs);
|
|
142
|
+
|
|
143
|
+
if (style === "only") {
|
|
144
|
+
log.info(
|
|
145
|
+
"Tip: check a folder header to include everything inside it, or expand it to pick specific files. Use the next step for paths deeper than what's shown.",
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
log.info(
|
|
149
|
+
"Tip: everything is selected at the folder level — uncheck a folder header to exclude it entirely, or expand it to exclude specific files. Use the next step for deeper paths.",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
92
152
|
|
|
93
|
-
|
|
153
|
+
// ── flat fallback when there are no dirs ──────────────────────────────────
|
|
154
|
+
if (dirNames.length === 0) {
|
|
155
|
+
const flatSelected = await multiselect({
|
|
156
|
+
message: "Choose paths:",
|
|
157
|
+
options: files.map((f) => ({ value: f, label: f })),
|
|
158
|
+
initialValues: style === "except" ? files : [],
|
|
159
|
+
required: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (isCancel(flatSelected)) {
|
|
163
|
+
cancel("Cancelled.");
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return flatSelected as string[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── build groupMultiselect options ────────────────────────────────────────
|
|
171
|
+
const groupOptions: Record<string, { value: string; label: string }[]> = {};
|
|
172
|
+
for (const [dir, children] of Object.entries(dirs)) {
|
|
173
|
+
groupOptions[dir] = children.map((child) => ({
|
|
174
|
+
value: child,
|
|
175
|
+
label: child.slice(child.indexOf("/") + 1),
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
if (files.length > 0) {
|
|
179
|
+
groupOptions.Files = files.map((f) => ({ value: f, label: f }));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// "except" → pre-select dir headers + root files (depth-1 only)
|
|
183
|
+
const initialValues = style === "except" ? [...dirNames, ...files] : [];
|
|
184
|
+
|
|
185
|
+
const rawSelected = await groupMultiselect({
|
|
94
186
|
message: "Choose paths:",
|
|
95
|
-
options:
|
|
187
|
+
options: groupOptions,
|
|
96
188
|
initialValues,
|
|
97
189
|
required: true,
|
|
190
|
+
selectableGroups: true,
|
|
98
191
|
});
|
|
99
192
|
|
|
100
|
-
if (isCancel(
|
|
193
|
+
if (isCancel(rawSelected)) {
|
|
101
194
|
cancel("Cancelled.");
|
|
102
195
|
process.exit(0);
|
|
103
196
|
}
|
|
104
197
|
|
|
105
|
-
|
|
198
|
+
const selected = normalizeSelection(rawSelected as string[], dirNames);
|
|
199
|
+
|
|
200
|
+
// ── path prompt loop for depth-3+ targets ────────────────────────────────
|
|
201
|
+
// Repeats until the user presses Enter on an empty input.
|
|
202
|
+
let result = selected;
|
|
203
|
+
const promptMsg =
|
|
204
|
+
style === "only" ? "Add a deeper path to include (press Enter to finish):" : "Exclude a deeper path (press Enter to finish):";
|
|
205
|
+
|
|
206
|
+
while (true) {
|
|
207
|
+
const deepPathRaw = await path({
|
|
208
|
+
message: promptMsg,
|
|
209
|
+
root: cwd,
|
|
210
|
+
validate: (value) => {
|
|
211
|
+
if (!value) return undefined; // empty = done, always valid
|
|
212
|
+
const relative = value.startsWith(`${cwd}/`) ? value.slice(cwd.length + 1) : value;
|
|
213
|
+
if (!relative) return "Path cannot be empty.";
|
|
214
|
+
if (!existsSync(join(cwd, relative))) return `Path not found: ${relative}`;
|
|
215
|
+
return undefined;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (isCancel(deepPathRaw)) {
|
|
220
|
+
cancel("Cancelled.");
|
|
221
|
+
process.exit(0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const deepPathAbsolute = deepPathRaw as string;
|
|
225
|
+
if (!deepPathAbsolute) break; // user skipped — done
|
|
226
|
+
|
|
227
|
+
const deepPath = deepPathAbsolute.startsWith(`${cwd}/`) ? deepPathAbsolute.slice(cwd.length + 1) : deepPathAbsolute;
|
|
228
|
+
|
|
229
|
+
if (!deepPath) break;
|
|
230
|
+
|
|
231
|
+
if (style === "only") {
|
|
232
|
+
result = [...new Set([...result, deepPath])];
|
|
233
|
+
} else {
|
|
234
|
+
result = expandExclusion(result, deepPath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return result;
|
|
106
239
|
}
|
|
107
240
|
|
|
108
241
|
// ─── Totopo mount path inside container ──────────────────────────────────────
|