@olwiba/dx 0.0.22 → 0.0.24
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 +74 -9
- package/dist/ascii-gif.js +7 -0
- package/dist/ascii-gif.js.map +1 -1
- package/dist/cli.js +562 -36
- package/dist/generate-assets.d.ts +6 -0
- package/dist/generate-assets.js +79 -13
- package/dist/generate-assets.js.map +1 -1
- package/dist/generate-previews.d.ts +19 -1
- package/dist/generate-previews.js +52 -11
- package/dist/generate-previews.js.map +1 -1
- package/dist/skills.d.ts +3 -1
- package/dist/skills.js +5 -0
- package/dist/skills.js.map +1 -1
- package/package.json +10 -6
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import { mkdirSync, writeFileSync, existsSync, realpathSync, readFileSync, lstatSync, readdirSync } from 'fs';
|
|
4
|
+
import path, { join, resolve, relative, sep, isAbsolute, basename } from 'path';
|
|
5
5
|
import { createInterface } from 'readline/promises';
|
|
6
6
|
import { stdout, stdin } from 'process';
|
|
7
|
+
import { createRequire } from 'module';
|
|
7
8
|
|
|
8
9
|
var __defProp = Object.defineProperty;
|
|
9
10
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -15,6 +16,414 @@ var __export = (target, all) => {
|
|
|
15
16
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
// src/worktree-cleanup.ts
|
|
20
|
+
var worktree_cleanup_exports = {};
|
|
21
|
+
__export(worktree_cleanup_exports, {
|
|
22
|
+
classifyWorktree: () => classifyWorktree,
|
|
23
|
+
parseWorktreeList: () => parseWorktreeList,
|
|
24
|
+
runWorktreeCleanup: () => runWorktreeCleanup
|
|
25
|
+
});
|
|
26
|
+
function parseWorktreeList(output3) {
|
|
27
|
+
return output3.trim().split(/\r?\n\r?\n/).filter(Boolean).map((record) => {
|
|
28
|
+
const fields = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const line of record.split(/\r?\n/)) {
|
|
30
|
+
const separator = line.indexOf(" ");
|
|
31
|
+
if (separator === -1) {
|
|
32
|
+
fields.set(line, "");
|
|
33
|
+
} else {
|
|
34
|
+
fields.set(line.slice(0, separator), line.slice(separator + 1));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const path2 = fields.get("worktree");
|
|
38
|
+
const head = fields.get("HEAD");
|
|
39
|
+
if (!path2 || !head) throw new Error("Unexpected output from git worktree list");
|
|
40
|
+
const branchRef = fields.get("branch");
|
|
41
|
+
return {
|
|
42
|
+
path: path2,
|
|
43
|
+
head,
|
|
44
|
+
...branchRef ? { branch: branchRef.replace(/^refs\/heads\//, "") } : {}
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function classifyWorktree(state) {
|
|
49
|
+
const reasons = [];
|
|
50
|
+
let removalReason;
|
|
51
|
+
if (state.isCurrent) reasons.push("current working directory");
|
|
52
|
+
if (!state.branch) {
|
|
53
|
+
if (state.headMerged) {
|
|
54
|
+
removalReason = `detached HEAD merged into ${state.defaultBranch}`;
|
|
55
|
+
} else {
|
|
56
|
+
reasons.push(`detached HEAD is not merged into ${state.defaultBranch}`);
|
|
57
|
+
}
|
|
58
|
+
} else if (state.remoteBranchExists) {
|
|
59
|
+
if (state.remoteBranchMerged) {
|
|
60
|
+
removalReason = `branch merged into ${state.defaultBranch}`;
|
|
61
|
+
} else {
|
|
62
|
+
reasons.push(`remote branch exists; merge into ${state.defaultBranch} not confirmed`);
|
|
63
|
+
}
|
|
64
|
+
} else if (!state.headMerged) {
|
|
65
|
+
reasons.push(`remote branch deleted; merge into ${state.defaultBranch} not confirmed`);
|
|
66
|
+
} else if (state.head === state.defaultSha) {
|
|
67
|
+
reasons.push(`branch points at current ${state.defaultBranch}; automatic removal skipped`);
|
|
68
|
+
} else {
|
|
69
|
+
removalReason = `branch merged into ${state.defaultBranch} (remote branch deleted)`;
|
|
70
|
+
}
|
|
71
|
+
if (!state.pathExists) reasons.push("worktree path missing");
|
|
72
|
+
if (state.dirty) reasons.push("uncommitted changes");
|
|
73
|
+
if (state.aheadCount > 0) {
|
|
74
|
+
reasons.push(
|
|
75
|
+
`${state.aheadCount} commit(s) ahead of ${state.remote}/${state.defaultBranch}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return reasons.length > 0 ? { removable: false, reasons } : { removable: true, reason: removalReason ?? "safe to remove" };
|
|
79
|
+
}
|
|
80
|
+
async function runWorktreeCleanup(args, hooks = {}) {
|
|
81
|
+
let options;
|
|
82
|
+
try {
|
|
83
|
+
options = parseCleanupArgs(args);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
86
|
+
`);
|
|
87
|
+
writeUsage(process.stderr);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
91
|
+
writeUsage(process.stdout);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
let repoPath;
|
|
95
|
+
try {
|
|
96
|
+
repoPath = resolveRepo(options.repo, options.reposRoot);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
99
|
+
`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const repoName = basename(repoPath);
|
|
103
|
+
process.stdout.write(`Repo: ${repoName}
|
|
104
|
+
Path: ${repoPath}
|
|
105
|
+
`);
|
|
106
|
+
if (options.fetch) {
|
|
107
|
+
process.stdout.write(`Fetching latest from ${options.remote}...
|
|
108
|
+
`);
|
|
109
|
+
const fetched = runGit(repoPath, ["fetch", options.remote, "--prune"], true);
|
|
110
|
+
if (!fetched.ok) {
|
|
111
|
+
process.stderr.write(fetched.stderr || `git fetch failed for ${repoName}
|
|
112
|
+
`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const defaultBranch = resolveDefaultBranch(repoPath, options.remote);
|
|
117
|
+
const defaultRef = `${options.remote}/${defaultBranch}`;
|
|
118
|
+
const defaultSha = gitOutput(repoPath, ["rev-parse", defaultRef]);
|
|
119
|
+
if (!defaultSha) {
|
|
120
|
+
process.stderr.write(`Could not find ${defaultRef} for ${repoName}.
|
|
121
|
+
`);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
const worktrees = parseWorktreeList(requiredGitOutput(repoPath, ["worktree", "list", "--porcelain"]));
|
|
125
|
+
const additionalWorktrees = worktrees.slice(1);
|
|
126
|
+
if (additionalWorktrees.length === 0) {
|
|
127
|
+
process.stdout.write("No additional worktrees found. Nothing to clean up.\n");
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const callerWorktree = canonicalPath(gitOutput(process.cwd(), ["rev-parse", "--show-toplevel"]));
|
|
131
|
+
const removable = [];
|
|
132
|
+
const preserved = [];
|
|
133
|
+
for (const worktree of additionalWorktrees) {
|
|
134
|
+
const pathExists = existsSync(worktree.path);
|
|
135
|
+
const trackedBranch = worktree.branch ? gitOutput(repoPath, [
|
|
136
|
+
"for-each-ref",
|
|
137
|
+
"--format=%(upstream:short)",
|
|
138
|
+
`refs/heads/${worktree.branch}`
|
|
139
|
+
]) : "";
|
|
140
|
+
const remoteBranch = worktree.branch ? trackedBranch.startsWith(`${options.remote}/`) ? trackedBranch : `${options.remote}/${worktree.branch}` : "";
|
|
141
|
+
const remoteBranchExists = Boolean(remoteBranch) && gitSucceeds(repoPath, ["rev-parse", "--verify", remoteBranch]);
|
|
142
|
+
const headMerged = gitIsAncestor(repoPath, worktree.head, defaultRef);
|
|
143
|
+
const classification = classifyWorktree({
|
|
144
|
+
branch: worktree.branch,
|
|
145
|
+
head: worktree.head,
|
|
146
|
+
defaultSha,
|
|
147
|
+
defaultBranch,
|
|
148
|
+
remote: options.remote,
|
|
149
|
+
isCurrent: canonicalPath(worktree.path) === callerWorktree,
|
|
150
|
+
pathExists,
|
|
151
|
+
dirty: pathExists && Boolean(gitOutput(worktree.path, ["status", "--porcelain"])),
|
|
152
|
+
aheadCount: worktree.branch && pathExists ? Number(gitOutput(worktree.path, ["rev-list", "--count", `${defaultRef}..HEAD`]) || 0) : 0,
|
|
153
|
+
remoteBranchExists,
|
|
154
|
+
remoteBranchMerged: remoteBranchExists && gitIsAncestor(repoPath, remoteBranch, defaultRef),
|
|
155
|
+
headMerged
|
|
156
|
+
});
|
|
157
|
+
if (classification.removable) {
|
|
158
|
+
removable.push({
|
|
159
|
+
...worktree,
|
|
160
|
+
reason: classification.reason,
|
|
161
|
+
sizeMB: directorySizeMB(worktree.path)
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
preserved.push({ ...worktree, reasons: classification.reasons });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (preserved.length > 0) {
|
|
168
|
+
process.stdout.write("\nWorktrees needing attention (not automatically removed):\n");
|
|
169
|
+
for (const item of preserved) {
|
|
170
|
+
process.stdout.write(` ${item.branch ?? "(detached)"}
|
|
171
|
+
${item.reasons.join("; ")}
|
|
172
|
+
${item.path}
|
|
173
|
+
`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (removable.length === 0) {
|
|
177
|
+
process.stdout.write(`
|
|
178
|
+
No worktrees are safely removable.
|
|
179
|
+
Summary: 0 safely removable; ${preserved.length} needing attention.
|
|
180
|
+
`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const totalMB = removable.reduce((total, item) => total + item.sizeMB, 0);
|
|
184
|
+
process.stdout.write("\nWorktrees to remove:\n");
|
|
185
|
+
for (const item of removable) {
|
|
186
|
+
process.stdout.write(` ${item.branch ?? "(detached)"} - ${item.reason} - ~${item.sizeMB} MB
|
|
187
|
+
${item.path}
|
|
188
|
+
`);
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write(`Total space to reclaim: ~${totalMB} MB
|
|
191
|
+
`);
|
|
192
|
+
if (options.dryRun) {
|
|
193
|
+
process.stdout.write(`
|
|
194
|
+
[DRY RUN] No changes made.
|
|
195
|
+
Summary: ${removable.length} safely removable; ${preserved.length} needing attention.
|
|
196
|
+
`);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
if (!options.force && !await confirmRemoval()) {
|
|
200
|
+
process.stdout.write(`Aborted.
|
|
201
|
+
Summary: 0 removed; ${preserved.length} needing attention.
|
|
202
|
+
`);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
let removedCount = 0;
|
|
206
|
+
let reclaimedMB = 0;
|
|
207
|
+
const skipped = [];
|
|
208
|
+
for (const item of removable) {
|
|
209
|
+
hooks.beforeRemoval?.(item);
|
|
210
|
+
const current = inspectWorktree(repoPath, item.path, options.remote, defaultBranch);
|
|
211
|
+
if (!current.classification.removable) {
|
|
212
|
+
skipped.push({
|
|
213
|
+
item,
|
|
214
|
+
error: `changed since planning: ${current.classification.reasons.join("; ")}`
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
process.stdout.write(`Removing worktree: ${item.branch ?? "(detached)"} ...
|
|
219
|
+
`);
|
|
220
|
+
const removal = runGit(repoPath, ["worktree", "remove", item.path, "--force"]);
|
|
221
|
+
if (!removal.ok) {
|
|
222
|
+
skipped.push({ item, error: removal.stderr.trim() || "git worktree remove failed" });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
removedCount++;
|
|
226
|
+
reclaimedMB += item.sizeMB;
|
|
227
|
+
hooks.afterWorktreeRemoval?.(item);
|
|
228
|
+
if (current.worktree.branch) {
|
|
229
|
+
const branchRemoval = runGit(repoPath, ["branch", "-D", current.worktree.branch]);
|
|
230
|
+
if (!branchRemoval.ok) {
|
|
231
|
+
skipped.push({
|
|
232
|
+
item,
|
|
233
|
+
error: `worktree removed, but local branch deletion failed: ${branchRemoval.stderr.trim() || "git branch -D failed"}`
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (skipped.length > 0) {
|
|
240
|
+
process.stdout.write("\nWorktree cleanup issues:\n");
|
|
241
|
+
for (const { item, error } of skipped) {
|
|
242
|
+
process.stdout.write(` ${item.branch ?? "(detached)"}: ${error}
|
|
243
|
+
${item.path}
|
|
244
|
+
`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
process.stdout.write(`
|
|
248
|
+
Done. Removed ${removedCount} worktree(s), reclaimed ~${reclaimedMB} MB; skipped ${skipped.length}.
|
|
249
|
+
`);
|
|
250
|
+
process.stdout.write(`Summary: ${preserved.length} worktree(s) still need attention (see report above).
|
|
251
|
+
`);
|
|
252
|
+
return skipped.length > 0 ? 1 : 0;
|
|
253
|
+
}
|
|
254
|
+
function inspectWorktree(repoPath, worktreePath, remote, defaultBranch) {
|
|
255
|
+
const listed = parseWorktreeList(requiredGitOutput(repoPath, ["worktree", "list", "--porcelain"]));
|
|
256
|
+
const expectedPath = pathIdentity(worktreePath);
|
|
257
|
+
const worktree = listed.find((candidate) => pathIdentity(candidate.path) === expectedPath) ?? { path: worktreePath, head: "", branch: void 0 };
|
|
258
|
+
const pathExists = existsSync(worktree.path);
|
|
259
|
+
const defaultRef = `${remote}/${defaultBranch}`;
|
|
260
|
+
const defaultSha = gitOutput(repoPath, ["rev-parse", defaultRef]);
|
|
261
|
+
const trackedBranch = worktree.branch ? gitOutput(repoPath, ["for-each-ref", "--format=%(upstream:short)", `refs/heads/${worktree.branch}`]) : "";
|
|
262
|
+
const remoteBranch = worktree.branch ? trackedBranch.startsWith(`${remote}/`) ? trackedBranch : `${remote}/${worktree.branch}` : "";
|
|
263
|
+
const remoteBranchExists = Boolean(remoteBranch) && gitSucceeds(repoPath, ["rev-parse", "--verify", remoteBranch]);
|
|
264
|
+
const currentWorktree = canonicalPath(gitOutput(process.cwd(), ["rev-parse", "--show-toplevel"]));
|
|
265
|
+
return {
|
|
266
|
+
worktree,
|
|
267
|
+
classification: classifyWorktree({
|
|
268
|
+
branch: worktree.branch,
|
|
269
|
+
head: worktree.head,
|
|
270
|
+
defaultSha,
|
|
271
|
+
defaultBranch,
|
|
272
|
+
remote,
|
|
273
|
+
isCurrent: expectedPath !== "" && expectedPath === currentWorktree,
|
|
274
|
+
pathExists,
|
|
275
|
+
dirty: pathExists && Boolean(gitOutput(worktree.path, ["status", "--porcelain"])),
|
|
276
|
+
aheadCount: worktree.branch && pathExists ? Number(gitOutput(worktree.path, ["rev-list", "--count", `${defaultRef}..HEAD`]) || 0) : 0,
|
|
277
|
+
remoteBranchExists,
|
|
278
|
+
remoteBranchMerged: remoteBranchExists && gitIsAncestor(repoPath, remoteBranch, defaultRef),
|
|
279
|
+
headMerged: Boolean(worktree.head) && gitIsAncestor(repoPath, worktree.head, defaultRef)
|
|
280
|
+
})
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function parseCleanupArgs(args) {
|
|
284
|
+
const options = {
|
|
285
|
+
remote: "origin",
|
|
286
|
+
dryRun: false,
|
|
287
|
+
force: false,
|
|
288
|
+
fetch: true
|
|
289
|
+
};
|
|
290
|
+
for (let index = 0; index < args.length; index++) {
|
|
291
|
+
const arg = args[index];
|
|
292
|
+
if (arg === "--dry-run" || arg.toLowerCase() === "-dryrun") options.dryRun = true;
|
|
293
|
+
else if (arg === "--force" || arg.toLowerCase() === "-force") options.force = true;
|
|
294
|
+
else if (arg === "--no-fetch") options.fetch = false;
|
|
295
|
+
else if (arg === "--help" || arg === "-h") continue;
|
|
296
|
+
else if (arg === "--repos-root" || arg === "--remote") {
|
|
297
|
+
const value = args[++index];
|
|
298
|
+
if (!value) throw new Error(`${arg} requires a value`);
|
|
299
|
+
if (arg === "--repos-root") options.reposRoot = value;
|
|
300
|
+
else options.remote = value;
|
|
301
|
+
} else if (arg.startsWith("--repos-root=")) options.reposRoot = arg.slice(13);
|
|
302
|
+
else if (arg.startsWith("--remote=")) options.remote = arg.slice(9);
|
|
303
|
+
else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`);
|
|
304
|
+
else if (!options.repo) options.repo = arg;
|
|
305
|
+
else throw new Error(`Unexpected argument: ${arg}`);
|
|
306
|
+
}
|
|
307
|
+
return options;
|
|
308
|
+
}
|
|
309
|
+
function resolveRepo(repoInput, reposRootInput) {
|
|
310
|
+
if (!repoInput) {
|
|
311
|
+
const root = gitOutput(process.cwd(), ["rev-parse", "--show-toplevel"]);
|
|
312
|
+
if (!root) throw new Error("Current directory is not inside a git repository. Provide a repo path or name.");
|
|
313
|
+
return realpathSync(root);
|
|
314
|
+
}
|
|
315
|
+
const directPath = isAbsolute(repoInput) ? repoInput : resolve(process.cwd(), repoInput);
|
|
316
|
+
if (existsSync(directPath)) return resolveGitRoot(directPath);
|
|
317
|
+
const reposRoot = resolve(process.cwd(), reposRootInput ?? "repos");
|
|
318
|
+
if (!existsSync(reposRoot)) {
|
|
319
|
+
throw new Error(`Repo '${repoInput}' was not found. Provide its path or use --repos-root.`);
|
|
320
|
+
}
|
|
321
|
+
const matches = findRepos(reposRoot).filter((path2) => basename(path2).toLowerCase() === repoInput.toLowerCase());
|
|
322
|
+
if (matches.length === 1) return matches[0];
|
|
323
|
+
if (matches.length > 1) throw new Error(`Repo name '${repoInput}' is ambiguous. Provide a repo path instead.`);
|
|
324
|
+
throw new Error(`Unknown repo '${repoInput}' under ${reposRoot}.`);
|
|
325
|
+
}
|
|
326
|
+
function resolveGitRoot(path2) {
|
|
327
|
+
const root = gitOutput(path2, ["rev-parse", "--show-toplevel"]);
|
|
328
|
+
if (!root) throw new Error(`Path is not inside a git repository: ${path2}`);
|
|
329
|
+
return realpathSync(root);
|
|
330
|
+
}
|
|
331
|
+
function findRepos(root) {
|
|
332
|
+
const repos = [];
|
|
333
|
+
const visit = (directory) => {
|
|
334
|
+
if (existsSync(join(directory, ".git"))) {
|
|
335
|
+
repos.push(realpathSync(directory));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
339
|
+
if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
340
|
+
visit(join(directory, entry.name));
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
visit(root);
|
|
344
|
+
return repos;
|
|
345
|
+
}
|
|
346
|
+
function resolveDefaultBranch(repoPath, remote) {
|
|
347
|
+
const symbolic = gitOutput(repoPath, ["symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`]);
|
|
348
|
+
if (symbolic) return symbolic.replace(`${remote}/`, "");
|
|
349
|
+
if (gitSucceeds(repoPath, ["rev-parse", "--verify", `${remote}/main`])) return "main";
|
|
350
|
+
if (gitSucceeds(repoPath, ["rev-parse", "--verify", `${remote}/master`])) return "master";
|
|
351
|
+
throw new Error(`Could not resolve the default branch for remote '${remote}'.`);
|
|
352
|
+
}
|
|
353
|
+
function canonicalPath(path2) {
|
|
354
|
+
if (!path2 || !existsSync(path2)) return "";
|
|
355
|
+
const canonical = realpathSync(path2);
|
|
356
|
+
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
357
|
+
}
|
|
358
|
+
function pathIdentity(path2) {
|
|
359
|
+
const identity = existsSync(path2) ? realpathSync(path2) : resolve(path2);
|
|
360
|
+
return process.platform === "win32" ? identity.toLowerCase() : identity;
|
|
361
|
+
}
|
|
362
|
+
function gitIsAncestor(repoPath, ancestor, descendant) {
|
|
363
|
+
return gitSucceeds(repoPath, ["merge-base", "--is-ancestor", ancestor, descendant]);
|
|
364
|
+
}
|
|
365
|
+
function gitSucceeds(repoPath, args) {
|
|
366
|
+
return runGit(repoPath, args).ok;
|
|
367
|
+
}
|
|
368
|
+
function requiredGitOutput(repoPath, args) {
|
|
369
|
+
const result = runGit(repoPath, args);
|
|
370
|
+
if (!result.ok) throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed`);
|
|
371
|
+
return result.stdout.trim();
|
|
372
|
+
}
|
|
373
|
+
function gitOutput(repoPath, args) {
|
|
374
|
+
const result = runGit(repoPath, args);
|
|
375
|
+
return result.ok ? result.stdout.trim() : "";
|
|
376
|
+
}
|
|
377
|
+
function runGit(repoPath, args, showOutput = false) {
|
|
378
|
+
const result = spawnSync("git", ["-C", repoPath, ...args], {
|
|
379
|
+
encoding: "utf8",
|
|
380
|
+
stdio: showOutput ? ["inherit", "pipe", "pipe"] : "pipe"
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
ok: !result.error && result.status === 0,
|
|
384
|
+
stdout: result.stdout ?? "",
|
|
385
|
+
stderr: result.error?.message ?? result.stderr ?? ""
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function directorySizeMB(path2) {
|
|
389
|
+
if (!existsSync(path2)) return 0;
|
|
390
|
+
let bytes = 0;
|
|
391
|
+
const visit = (entryPath) => {
|
|
392
|
+
try {
|
|
393
|
+
const stat = lstatSync(entryPath);
|
|
394
|
+
if (stat.isSymbolicLink()) return;
|
|
395
|
+
if (stat.isFile()) {
|
|
396
|
+
bytes += stat.size;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (stat.isDirectory()) {
|
|
400
|
+
for (const entry of readdirSync(entryPath)) visit(join(entryPath, entry));
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
visit(path2);
|
|
406
|
+
return Math.round(bytes / 1024 / 1024);
|
|
407
|
+
}
|
|
408
|
+
async function confirmRemoval() {
|
|
409
|
+
const readline = createInterface({ input: stdin, output: stdout });
|
|
410
|
+
try {
|
|
411
|
+
const answer = await readline.question("\nRemove these worktrees? (y/N) ");
|
|
412
|
+
return ["y", "yes"].includes(answer.trim().toLowerCase());
|
|
413
|
+
} finally {
|
|
414
|
+
readline.close();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function writeUsage(stream) {
|
|
418
|
+
stream.write(
|
|
419
|
+
"Usage: dx worktree cleanup [repo-name-or-path] [--repos-root <path>] [--remote <name>] [--dry-run] [--force] [--no-fetch]\n\nWith no repo, the current git repository is used. A repo name is searched for under ./repos by default.\n"
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
var init_worktree_cleanup = __esm({
|
|
423
|
+
"src/worktree-cleanup.ts"() {
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
18
427
|
// src/ascii/compose.ts
|
|
19
428
|
function composeAsciiText(font, text) {
|
|
20
429
|
const charBlocks = [];
|
|
@@ -1517,6 +1926,7 @@ function getGlitterIntensity(cell, loop) {
|
|
|
1517
1926
|
return Math.max(0.55, Math.min(1, 0.68 + flow + sparkBoost));
|
|
1518
1927
|
}
|
|
1519
1928
|
function createRowColorPalette(options) {
|
|
1929
|
+
assertGifPaletteSize("row-color", 1 + options.rowColors.length * options.levels);
|
|
1520
1930
|
const bg = parseColor(options.backgroundColor);
|
|
1521
1931
|
const blend = parseColor(options.blendColor);
|
|
1522
1932
|
const table = [bg.r, bg.g, bg.b];
|
|
@@ -1557,6 +1967,7 @@ function resolveFont(font) {
|
|
|
1557
1967
|
return readFileSync(font, "utf-8");
|
|
1558
1968
|
}
|
|
1559
1969
|
function createMultiAccentPalette(options) {
|
|
1970
|
+
assertGifPaletteSize("accent", 1 + (1 + options.accentColors.length) * options.levels);
|
|
1560
1971
|
const bg = parseColor(options.backgroundColor);
|
|
1561
1972
|
const blend = parseColor(options.blendColor);
|
|
1562
1973
|
const base = parseColor(options.color);
|
|
@@ -1577,8 +1988,13 @@ function createMultiAccentPalette(options) {
|
|
|
1577
1988
|
while (table.length < 256 * 3) table.push(0, 0, 0);
|
|
1578
1989
|
return { table: Uint8Array.from(table.slice(0, 256 * 3)) };
|
|
1579
1990
|
}
|
|
1580
|
-
function
|
|
1581
|
-
|
|
1991
|
+
function assertGifPaletteSize(mode, entries) {
|
|
1992
|
+
if (entries > 256) {
|
|
1993
|
+
throw new Error(`GIF palette supports at most 256 entries; ${mode} mode requires ${entries}`);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function parseColor(input3) {
|
|
1997
|
+
const color = input3.trim();
|
|
1582
1998
|
const shortHex = /^#([0-9a-f]{3})$/i.exec(color);
|
|
1583
1999
|
if (shortHex) {
|
|
1584
2000
|
const [r, g, b] = shortHex[1].split("").map((part) => Number.parseInt(part + part, 16));
|
|
@@ -1601,7 +2017,7 @@ function parseColor(input2) {
|
|
|
1601
2017
|
b: clampByte(Number.parseInt(rgb[3], 10))
|
|
1602
2018
|
};
|
|
1603
2019
|
}
|
|
1604
|
-
throw new Error(`Unsupported color "${
|
|
2020
|
+
throw new Error(`Unsupported color "${input3}". Use #rgb, #rrggbb, or rgb(r,g,b).`);
|
|
1605
2021
|
}
|
|
1606
2022
|
function mix(a, b, amount) {
|
|
1607
2023
|
return {
|
|
@@ -1645,8 +2061,8 @@ function lzwEncode(indices, minCodeSize) {
|
|
|
1645
2061
|
const clearCode = 1 << minCodeSize;
|
|
1646
2062
|
const endCode = clearCode + 1;
|
|
1647
2063
|
let codeSize = minCodeSize + 1;
|
|
1648
|
-
const
|
|
1649
|
-
const writeCode = createBitWriter(
|
|
2064
|
+
const output3 = [];
|
|
2065
|
+
const writeCode = createBitWriter(output3);
|
|
1650
2066
|
writeCode(clearCode, codeSize);
|
|
1651
2067
|
let codesSinceClear = 0;
|
|
1652
2068
|
for (const index of indices) {
|
|
@@ -1660,14 +2076,14 @@ function lzwEncode(indices, minCodeSize) {
|
|
|
1660
2076
|
}
|
|
1661
2077
|
writeCode(endCode, codeSize);
|
|
1662
2078
|
writeCode(-1, 0);
|
|
1663
|
-
return Uint8Array.from(
|
|
2079
|
+
return Uint8Array.from(output3);
|
|
1664
2080
|
}
|
|
1665
|
-
function createBitWriter(
|
|
2081
|
+
function createBitWriter(output3) {
|
|
1666
2082
|
let buffer = 0;
|
|
1667
2083
|
let bitCount = 0;
|
|
1668
2084
|
return (code, size) => {
|
|
1669
2085
|
if (code < 0) {
|
|
1670
|
-
if (bitCount > 0)
|
|
2086
|
+
if (bitCount > 0) output3.push(buffer & 255);
|
|
1671
2087
|
buffer = 0;
|
|
1672
2088
|
bitCount = 0;
|
|
1673
2089
|
return;
|
|
@@ -1675,7 +2091,7 @@ function createBitWriter(output2) {
|
|
|
1675
2091
|
buffer |= code << bitCount;
|
|
1676
2092
|
bitCount += size;
|
|
1677
2093
|
while (bitCount >= 8) {
|
|
1678
|
-
|
|
2094
|
+
output3.push(buffer & 255);
|
|
1679
2095
|
buffer >>= 8;
|
|
1680
2096
|
bitCount -= 8;
|
|
1681
2097
|
}
|
|
@@ -1740,6 +2156,14 @@ function getSvgInner(svgPath) {
|
|
|
1740
2156
|
const inner = svg.replace(/<svg[^>]*>/, "").replace(/<\/svg>/, "").trim();
|
|
1741
2157
|
return { inner, viewBox };
|
|
1742
2158
|
}
|
|
2159
|
+
function resolveIcon(icon) {
|
|
2160
|
+
const mode = detectIconMode(icon);
|
|
2161
|
+
if (mode === "svg") {
|
|
2162
|
+
const { inner, viewBox } = getSvgInner(icon);
|
|
2163
|
+
return { mode, inner, viewBox };
|
|
2164
|
+
}
|
|
2165
|
+
return { mode, inner: getLucideInner(icon), viewBox: 24 };
|
|
2166
|
+
}
|
|
1743
2167
|
function buildIconSvgLucide(inner, color, size) {
|
|
1744
2168
|
const pad = Math.round(size * 0.18);
|
|
1745
2169
|
const area = size - pad * 2;
|
|
@@ -1798,8 +2222,44 @@ function buildOgSvgCustom(inner, viewBox, color) {
|
|
|
1798
2222
|
</g>
|
|
1799
2223
|
</svg>`;
|
|
1800
2224
|
}
|
|
2225
|
+
function buildOgGlowSvg(box) {
|
|
2226
|
+
const spread = 40;
|
|
2227
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
|
|
2228
|
+
<defs>
|
|
2229
|
+
<filter id="glow" x="-40%" y="-40%" width="180%" height="180%">
|
|
2230
|
+
<feGaussianBlur stdDeviation="46"/>
|
|
2231
|
+
</filter>
|
|
2232
|
+
</defs>
|
|
2233
|
+
<rect x="${box.left - spread}" y="${box.top - spread}" width="${box.width + spread * 2}" height="${box.height + spread * 2}" rx="48" fill="#ffffff" opacity="0.22" filter="url(#glow)"/>
|
|
2234
|
+
</svg>`;
|
|
2235
|
+
}
|
|
2236
|
+
function buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) {
|
|
2237
|
+
const markSize = OG_HEADER.markSize;
|
|
2238
|
+
const gap = 28;
|
|
2239
|
+
const fontSize = 56;
|
|
2240
|
+
const groupY = OG_HEADER.top;
|
|
2241
|
+
const scale = markSize / viewBox;
|
|
2242
|
+
const textWidth = Math.round(name.length * fontSize * 0.58);
|
|
2243
|
+
const totalWidth = markSize + gap + textWidth;
|
|
2244
|
+
const groupX = Math.round((1200 - totalWidth) / 2);
|
|
2245
|
+
const logoMarkup = mode === "lucide" ? (() => {
|
|
2246
|
+
const sw = (2.5 / scale).toFixed(4);
|
|
2247
|
+
return `<g transform="translate(${groupX} ${groupY}) scale(${scale})" stroke="white" fill="none" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">
|
|
2248
|
+
${inner}
|
|
2249
|
+
</g>`;
|
|
2250
|
+
})() : `<g transform="translate(${groupX} ${groupY}) scale(${scale})">
|
|
2251
|
+
${inner}
|
|
2252
|
+
</g>`;
|
|
2253
|
+
const textX = groupX + markSize + gap;
|
|
2254
|
+
const textY = groupY + markSize / 2;
|
|
2255
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
|
|
2256
|
+
<rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="${color}"/>
|
|
2257
|
+
${logoMarkup}
|
|
2258
|
+
<text x="${textX}" y="${textY}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="${fontSize}" font-weight="700" fill="white" dominant-baseline="middle" letter-spacing="-1">${escapeXml(name)}</text>
|
|
2259
|
+
</svg>`;
|
|
2260
|
+
}
|
|
1801
2261
|
async function generateAssets(config) {
|
|
1802
|
-
const { name, icon, color, outputDir } = config;
|
|
2262
|
+
const { name, icon, color, outputDir, ogComponent } = config;
|
|
1803
2263
|
let sharpFn;
|
|
1804
2264
|
try {
|
|
1805
2265
|
sharpFn = (await import('sharp')).default;
|
|
@@ -1811,17 +2271,8 @@ async function generateAssets(config) {
|
|
|
1811
2271
|
mkdirSync(outputDir, { recursive: true });
|
|
1812
2272
|
const faviconDir = join(outputDir, "favicon");
|
|
1813
2273
|
mkdirSync(faviconDir, { recursive: true });
|
|
1814
|
-
const mode = detectIconMode(icon);
|
|
1815
2274
|
const written = [];
|
|
1816
|
-
|
|
1817
|
-
let viewBox = 24;
|
|
1818
|
-
if (mode === "svg") {
|
|
1819
|
-
const parsed = getSvgInner(icon);
|
|
1820
|
-
inner = parsed.inner;
|
|
1821
|
-
viewBox = parsed.viewBox;
|
|
1822
|
-
} else {
|
|
1823
|
-
inner = getLucideInner(icon);
|
|
1824
|
-
}
|
|
2275
|
+
const { mode, inner, viewBox } = resolveIcon(icon);
|
|
1825
2276
|
const buildIcon = (size) => mode === "svg" ? buildIconSvgCustom(inner, viewBox, color, size) : buildIconSvgLucide(inner, color, size);
|
|
1826
2277
|
for (const size of FAVICON_SIZES) {
|
|
1827
2278
|
const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer();
|
|
@@ -1835,8 +2286,31 @@ async function generateAssets(config) {
|
|
|
1835
2286
|
writeFileSync(dest, png);
|
|
1836
2287
|
written.push(dest);
|
|
1837
2288
|
}
|
|
1838
|
-
const ogSvg = mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
|
|
1839
|
-
|
|
2289
|
+
const ogSvg = ogComponent ? buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) : mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
|
|
2290
|
+
let ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer();
|
|
2291
|
+
if (ogComponent) {
|
|
2292
|
+
const sideMargin = 72;
|
|
2293
|
+
const maxWidth = OG_WIDTH - sideMargin * 2;
|
|
2294
|
+
const headerBottom = OG_HEADER.top + OG_HEADER.markSize + OG_HEADER.gapBelow;
|
|
2295
|
+
const componentBuf = readFileSync(resolve(process.cwd(), ogComponent));
|
|
2296
|
+
const resized = await sharpFn(componentBuf, { density: 300 }).resize({ width: maxWidth, withoutEnlargement: true }).png().toBuffer();
|
|
2297
|
+
const { width: compWidth = maxWidth, height: compHeight = 0 } = await sharpFn(resized).metadata();
|
|
2298
|
+
const left = Math.round((OG_WIDTH - compWidth) / 2);
|
|
2299
|
+
const fits = headerBottom + compHeight <= OG_HEIGHT;
|
|
2300
|
+
const top = fits ? Math.round(headerBottom + (OG_HEIGHT - headerBottom - compHeight) / 2) : headerBottom;
|
|
2301
|
+
ogPng = await sharpFn(ogPng).composite([
|
|
2302
|
+
// Glow first: it has to sit under the component, and sharp applies
|
|
2303
|
+
// composites in array order.
|
|
2304
|
+
{
|
|
2305
|
+
input: Buffer.from(
|
|
2306
|
+
buildOgGlowSvg({ left, top, width: compWidth, height: compHeight })
|
|
2307
|
+
),
|
|
2308
|
+
left: 0,
|
|
2309
|
+
top: 0
|
|
2310
|
+
},
|
|
2311
|
+
{ input: resized, left, top }
|
|
2312
|
+
]).png().toBuffer();
|
|
2313
|
+
}
|
|
1840
2314
|
const ogDest = join(outputDir, "og-image.png");
|
|
1841
2315
|
writeFileSync(ogDest, ogPng);
|
|
1842
2316
|
written.push(ogDest);
|
|
@@ -1859,9 +2333,17 @@ async function generateAssets(config) {
|
|
|
1859
2333
|
written.push(robotsDest);
|
|
1860
2334
|
return { files: written };
|
|
1861
2335
|
}
|
|
1862
|
-
var FAVICON_SIZES, NAMED_ICONS;
|
|
2336
|
+
var OG_WIDTH, OG_HEIGHT, OG_HEADER, FAVICON_SIZES, NAMED_ICONS;
|
|
1863
2337
|
var init_generate_assets = __esm({
|
|
1864
2338
|
"src/generate-assets.ts"() {
|
|
2339
|
+
OG_WIDTH = 1200;
|
|
2340
|
+
OG_HEIGHT = 630;
|
|
2341
|
+
OG_HEADER = {
|
|
2342
|
+
top: 54,
|
|
2343
|
+
markSize: 96,
|
|
2344
|
+
/** Clear space between the wordmark's baseline and the component. */
|
|
2345
|
+
gapBelow: 30
|
|
2346
|
+
};
|
|
1865
2347
|
FAVICON_SIZES = [16, 32, 48, 64, 192, 512];
|
|
1866
2348
|
NAMED_ICONS = [
|
|
1867
2349
|
["apple-touch-icon.png", 180],
|
|
@@ -1870,17 +2352,33 @@ var init_generate_assets = __esm({
|
|
|
1870
2352
|
];
|
|
1871
2353
|
}
|
|
1872
2354
|
});
|
|
2355
|
+
|
|
2356
|
+
// src/skills.ts
|
|
2357
|
+
function isSafeSkillSlug(slug) {
|
|
2358
|
+
return slug !== "." && slug !== ".." && /^[A-Za-z0-9._-]+$/.test(slug);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// src/cli.ts
|
|
1873
2362
|
var DEFAULT_SOURCE = "https://olwiba.com/skills/manifest.json";
|
|
1874
2363
|
var [command, subcommand] = process.argv.slice(2);
|
|
1875
2364
|
if (command === "skills" && subcommand === "install") {
|
|
1876
2365
|
await runSkillsInstall();
|
|
2366
|
+
} else if ((command === "worktree" || command === "wt") && subcommand === "cleanup") {
|
|
2367
|
+
const { runWorktreeCleanup: runWorktreeCleanup2 } = await Promise.resolve().then(() => (init_worktree_cleanup(), worktree_cleanup_exports));
|
|
2368
|
+
try {
|
|
2369
|
+
process.exitCode = await runWorktreeCleanup2(process.argv.slice(4));
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2372
|
+
`);
|
|
2373
|
+
process.exitCode = 1;
|
|
2374
|
+
}
|
|
1877
2375
|
} else if (command === "ascii-gif") {
|
|
1878
2376
|
await runAsciiGif();
|
|
1879
2377
|
} else if (command === "generate-assets") {
|
|
1880
2378
|
await runGenerateAssets();
|
|
1881
2379
|
} else {
|
|
1882
2380
|
process.stdout.write(
|
|
1883
|
-
"Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>]\n"
|
|
2381
|
+
"Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx worktree cleanup [repo-name-or-path] [--repos-root <path>] [--remote <name>] [--dry-run] [--force] [--no-fetch]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n"
|
|
1884
2382
|
);
|
|
1885
2383
|
}
|
|
1886
2384
|
async function runSkillsInstall() {
|
|
@@ -1894,7 +2392,7 @@ async function runSkillsInstall() {
|
|
|
1894
2392
|
return;
|
|
1895
2393
|
}
|
|
1896
2394
|
const targetDir = target === "amp" ? join(".amp", "skills") : join(".claude", "skills");
|
|
1897
|
-
process.stdout.write(`Fetching manifest from ${source}
|
|
2395
|
+
process.stdout.write(`Fetching manifest from ${safeUrlForDisplay(source)}
|
|
1898
2396
|
`);
|
|
1899
2397
|
let manifest;
|
|
1900
2398
|
try {
|
|
@@ -1902,8 +2400,7 @@ async function runSkillsInstall() {
|
|
|
1902
2400
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1903
2401
|
manifest = await res.json();
|
|
1904
2402
|
} catch (err) {
|
|
1905
|
-
|
|
1906
|
-
process.stderr.write(`Failed to fetch manifest: ${message}
|
|
2403
|
+
process.stderr.write(`Failed to fetch manifest: ${safeRequestError(err)}
|
|
1907
2404
|
`);
|
|
1908
2405
|
process.exitCode = 1;
|
|
1909
2406
|
return;
|
|
@@ -1922,10 +2419,22 @@ async function runSkillsInstall() {
|
|
|
1922
2419
|
let installed = 0;
|
|
1923
2420
|
let failed = 0;
|
|
1924
2421
|
for (const skill of selected) {
|
|
1925
|
-
|
|
2422
|
+
if (!isSafeSkillSlug(skill.slug)) {
|
|
2423
|
+
process.stderr.write(" \u2717 invalid skill slug\n");
|
|
2424
|
+
failed++;
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
const skillDir = resolve(installDir, skill.slug);
|
|
2428
|
+
const relativeDestination = relative(installDir, skillDir);
|
|
2429
|
+
if (!relativeDestination || relativeDestination === ".." || relativeDestination.startsWith(`..${sep}`) || isAbsolute(relativeDestination)) {
|
|
2430
|
+
process.stderr.write(` \u2717 ${skill.slug}: invalid install destination
|
|
2431
|
+
`);
|
|
2432
|
+
failed++;
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
1926
2435
|
const skillPath = join(skillDir, "SKILL.md");
|
|
1927
|
-
const url = new URL(skill.contentUrl, source).toString();
|
|
1928
2436
|
try {
|
|
2437
|
+
const url = new URL(skill.contentUrl, source).toString();
|
|
1929
2438
|
const res = await fetch(url);
|
|
1930
2439
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1931
2440
|
const content = await res.text();
|
|
@@ -1935,8 +2444,7 @@ async function runSkillsInstall() {
|
|
|
1935
2444
|
`);
|
|
1936
2445
|
installed++;
|
|
1937
2446
|
} catch (err) {
|
|
1938
|
-
|
|
1939
|
-
process.stderr.write(` \u2717 ${skill.slug}: ${message}
|
|
2447
|
+
process.stderr.write(` \u2717 ${skill.slug}: ${safeRequestError(err)}
|
|
1940
2448
|
`);
|
|
1941
2449
|
failed++;
|
|
1942
2450
|
}
|
|
@@ -1946,6 +2454,23 @@ ${installed} installed, ${failed} failed
|
|
|
1946
2454
|
`);
|
|
1947
2455
|
process.stdout.write(`Location: ${targetDir}/
|
|
1948
2456
|
`);
|
|
2457
|
+
if (failed > 0) process.exitCode = 1;
|
|
2458
|
+
}
|
|
2459
|
+
function safeUrlForDisplay(value) {
|
|
2460
|
+
try {
|
|
2461
|
+
const url = new URL(value);
|
|
2462
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "custom source";
|
|
2463
|
+
url.username = "";
|
|
2464
|
+
url.password = "";
|
|
2465
|
+
url.search = "";
|
|
2466
|
+
url.hash = "";
|
|
2467
|
+
return url.toString();
|
|
2468
|
+
} catch {
|
|
2469
|
+
return "custom source";
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
function safeRequestError(error) {
|
|
2473
|
+
return error instanceof Error && /^HTTP \d{3}$/.test(error.message) ? error.message : "request failed";
|
|
1949
2474
|
}
|
|
1950
2475
|
async function selectSkills(skills, flags) {
|
|
1951
2476
|
if (flags.all === "true") return skills;
|
|
@@ -2044,9 +2569,10 @@ async function runGenerateAssets() {
|
|
|
2044
2569
|
const icon = flags.icon;
|
|
2045
2570
|
const color = flags.color;
|
|
2046
2571
|
const outputDir = flags.out ?? flags.output ?? "public";
|
|
2572
|
+
const ogComponent = flags["og-component"];
|
|
2047
2573
|
if (!name || !icon || !color) {
|
|
2048
2574
|
process.stderr.write(
|
|
2049
|
-
"Usage: dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>]\n"
|
|
2575
|
+
"Usage: dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n"
|
|
2050
2576
|
);
|
|
2051
2577
|
process.exitCode = 1;
|
|
2052
2578
|
return;
|
|
@@ -2054,7 +2580,7 @@ async function runGenerateAssets() {
|
|
|
2054
2580
|
process.stdout.write(`Generating assets for "${name}"\u2026
|
|
2055
2581
|
`);
|
|
2056
2582
|
try {
|
|
2057
|
-
const result = await generateAssets2({ name, icon, color, outputDir });
|
|
2583
|
+
const result = await generateAssets2({ name, icon, color, outputDir, ogComponent });
|
|
2058
2584
|
process.stdout.write(`Generated ${result.files.length} files in ${outputDir}/
|
|
2059
2585
|
`);
|
|
2060
2586
|
for (const f of result.files) process.stdout.write(` ${f}
|