@olwiba/dx 0.0.23 → 0.0.27
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 +44 -9
- package/dist/ascii-gif.js +7 -0
- package/dist/ascii-gif.js.map +1 -1
- package/dist/cli.js +659 -20
- package/dist/env-check.d.ts +66 -0
- package/dist/env-check.js +125 -0
- package/dist/env-check.js.map +1 -0
- 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 +14 -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
|
}
|
|
@@ -1936,17 +2352,169 @@ var init_generate_assets = __esm({
|
|
|
1936
2352
|
];
|
|
1937
2353
|
}
|
|
1938
2354
|
});
|
|
2355
|
+
|
|
2356
|
+
// src/env-check.ts
|
|
2357
|
+
var env_check_exports = {};
|
|
2358
|
+
__export(env_check_exports, {
|
|
2359
|
+
checkEnv: () => checkEnv,
|
|
2360
|
+
formatEnvReport: () => formatEnvReport,
|
|
2361
|
+
parseEnv: () => parseEnv
|
|
2362
|
+
});
|
|
2363
|
+
function parseEnv(source) {
|
|
2364
|
+
const keys = /* @__PURE__ */ new Map();
|
|
2365
|
+
const duplicates = [];
|
|
2366
|
+
const malformed = [];
|
|
2367
|
+
source.split(/\r?\n/).forEach((raw, index) => {
|
|
2368
|
+
const line = raw.trim();
|
|
2369
|
+
if (line === "" || line.startsWith("#")) return;
|
|
2370
|
+
const withoutExport = line.replace(/^export\s+/, "");
|
|
2371
|
+
const eq = withoutExport.indexOf("=");
|
|
2372
|
+
if (eq <= 0) {
|
|
2373
|
+
malformed.push({ line: index + 1, text: withoutExport.slice(0, 24) });
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
2377
|
+
const value = withoutExport.slice(eq + 1).trim();
|
|
2378
|
+
if (keys.has(key)) duplicates.push(key);
|
|
2379
|
+
keys.set(key, value !== "" && value !== '""' && value !== "''");
|
|
2380
|
+
});
|
|
2381
|
+
return { keys, duplicates, malformed };
|
|
2382
|
+
}
|
|
2383
|
+
function findRenameCandidate(unknownKey, knownKeys) {
|
|
2384
|
+
const normalise = (key) => key.replace(/[^A-Z0-9]/gi, "").toUpperCase();
|
|
2385
|
+
const target = normalise(unknownKey);
|
|
2386
|
+
return knownKeys.find((known) => {
|
|
2387
|
+
const candidate = normalise(known);
|
|
2388
|
+
if (candidate === target) return true;
|
|
2389
|
+
return candidate.length >= 6 && (target.endsWith(candidate) || target.startsWith(candidate));
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
function checkEnv({
|
|
2393
|
+
example,
|
|
2394
|
+
actual,
|
|
2395
|
+
/** Keys allowed to be absent, e.g. optional credentials. */
|
|
2396
|
+
optional = []
|
|
2397
|
+
}) {
|
|
2398
|
+
const exampleEnv = parseEnv(example);
|
|
2399
|
+
const actualEnv = parseEnv(actual);
|
|
2400
|
+
const optionalSet = new Set(optional);
|
|
2401
|
+
const exampleKeys = [...exampleEnv.keys.keys()];
|
|
2402
|
+
const findings = [];
|
|
2403
|
+
let okCount = 0;
|
|
2404
|
+
for (const key of exampleKeys) {
|
|
2405
|
+
if (!actualEnv.keys.has(key)) {
|
|
2406
|
+
if (!optionalSet.has(key)) findings.push({ kind: "missing", key });
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
const hasValue = actualEnv.keys.get(key) === true;
|
|
2410
|
+
const exampleHasValue = exampleEnv.keys.get(key) === true;
|
|
2411
|
+
if (!hasValue && exampleHasValue && !optionalSet.has(key)) {
|
|
2412
|
+
findings.push({ kind: "empty", key });
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
okCount += 1;
|
|
2416
|
+
}
|
|
2417
|
+
for (const key of actualEnv.keys.keys()) {
|
|
2418
|
+
if (exampleEnv.keys.has(key)) continue;
|
|
2419
|
+
const looksLike = findRenameCandidate(key, exampleKeys);
|
|
2420
|
+
findings.push(looksLike ? { kind: "renamed", key, looksLike } : { kind: "unknown", key });
|
|
2421
|
+
}
|
|
2422
|
+
for (const key of actualEnv.duplicates) findings.push({ kind: "duplicate", key });
|
|
2423
|
+
for (const line of actualEnv.malformed) findings.push({ kind: "malformed", ...line });
|
|
2424
|
+
return {
|
|
2425
|
+
findings,
|
|
2426
|
+
okCount,
|
|
2427
|
+
exampleCount: exampleEnv.keys.size,
|
|
2428
|
+
actualCount: actualEnv.keys.size
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
function formatEnvReport(result) {
|
|
2432
|
+
const lines = [];
|
|
2433
|
+
lines.push(
|
|
2434
|
+
` ${result.actualCount} keys checked against ${result.exampleCount} in the example, ${result.okCount} correct`
|
|
2435
|
+
);
|
|
2436
|
+
if (result.findings.length === 0) {
|
|
2437
|
+
lines.push("\nPASS \u2014 environment matches the example.");
|
|
2438
|
+
return lines.join("\n");
|
|
2439
|
+
}
|
|
2440
|
+
for (const kind of ORDER) {
|
|
2441
|
+
const group = result.findings.filter((finding) => finding.kind === kind);
|
|
2442
|
+
if (group.length === 0) continue;
|
|
2443
|
+
lines.push(`
|
|
2444
|
+
${LABELS[kind]}:`);
|
|
2445
|
+
for (const finding of group) {
|
|
2446
|
+
if (finding.kind === "renamed") {
|
|
2447
|
+
lines.push(` \u2717 ${finding.key} \u2192 did you mean ${finding.looksLike}?`);
|
|
2448
|
+
} else if (finding.kind === "malformed") {
|
|
2449
|
+
lines.push(` \u2717 line ${finding.line}: ${finding.text}\u2026`);
|
|
2450
|
+
} else {
|
|
2451
|
+
lines.push(` \u2717 ${finding.key}`);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
const renamed = result.findings.filter((finding) => finding.kind === "renamed").length;
|
|
2456
|
+
if (renamed > 0) {
|
|
2457
|
+
lines.push(
|
|
2458
|
+
`
|
|
2459
|
+
${renamed} key(s) look renamed. The setting they were meant to carry is unset,
|
|
2460
|
+
which usually means a default is in force that nobody chose.`
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
lines.push(`
|
|
2464
|
+
FAIL \u2014 ${result.findings.length} finding(s).`);
|
|
2465
|
+
return lines.join("\n");
|
|
2466
|
+
}
|
|
2467
|
+
var ORDER, LABELS;
|
|
2468
|
+
var init_env_check = __esm({
|
|
2469
|
+
"src/env-check.ts"() {
|
|
2470
|
+
ORDER = [
|
|
2471
|
+
"renamed",
|
|
2472
|
+
"missing",
|
|
2473
|
+
"empty",
|
|
2474
|
+
"malformed",
|
|
2475
|
+
"duplicate",
|
|
2476
|
+
"unknown"
|
|
2477
|
+
];
|
|
2478
|
+
LABELS = {
|
|
2479
|
+
renamed: "Renamed \u2014 set under a name nothing reads",
|
|
2480
|
+
missing: "Missing",
|
|
2481
|
+
empty: "Present but empty",
|
|
2482
|
+
malformed: "Malformed line",
|
|
2483
|
+
duplicate: "Set more than once",
|
|
2484
|
+
unknown: "Unknown \u2014 not in the example"
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
// src/skills.ts
|
|
2490
|
+
function isSafeSkillSlug(slug) {
|
|
2491
|
+
return slug !== "." && slug !== ".." && /^[A-Za-z0-9._-]+$/.test(slug);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// src/cli.ts
|
|
2495
|
+
var BREAK = "\n";
|
|
1939
2496
|
var DEFAULT_SOURCE = "https://olwiba.com/skills/manifest.json";
|
|
1940
2497
|
var [command, subcommand] = process.argv.slice(2);
|
|
1941
2498
|
if (command === "skills" && subcommand === "install") {
|
|
1942
2499
|
await runSkillsInstall();
|
|
2500
|
+
} else if ((command === "worktree" || command === "wt") && subcommand === "cleanup") {
|
|
2501
|
+
const { runWorktreeCleanup: runWorktreeCleanup2 } = await Promise.resolve().then(() => (init_worktree_cleanup(), worktree_cleanup_exports));
|
|
2502
|
+
try {
|
|
2503
|
+
process.exitCode = await runWorktreeCleanup2(process.argv.slice(4));
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2506
|
+
`);
|
|
2507
|
+
process.exitCode = 1;
|
|
2508
|
+
}
|
|
1943
2509
|
} else if (command === "ascii-gif") {
|
|
1944
2510
|
await runAsciiGif();
|
|
1945
2511
|
} else if (command === "generate-assets") {
|
|
1946
2512
|
await runGenerateAssets();
|
|
2513
|
+
} else if (command === "env-check" || command === "env") {
|
|
2514
|
+
process.exitCode = await runEnvCheck();
|
|
1947
2515
|
} else {
|
|
1948
2516
|
process.stdout.write(
|
|
1949
|
-
"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>] [--og-component <svg-or-image-path>]\n"
|
|
2517
|
+
"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"
|
|
1950
2518
|
);
|
|
1951
2519
|
}
|
|
1952
2520
|
async function runSkillsInstall() {
|
|
@@ -1960,7 +2528,7 @@ async function runSkillsInstall() {
|
|
|
1960
2528
|
return;
|
|
1961
2529
|
}
|
|
1962
2530
|
const targetDir = target === "amp" ? join(".amp", "skills") : join(".claude", "skills");
|
|
1963
|
-
process.stdout.write(`Fetching manifest from ${source}
|
|
2531
|
+
process.stdout.write(`Fetching manifest from ${safeUrlForDisplay(source)}
|
|
1964
2532
|
`);
|
|
1965
2533
|
let manifest;
|
|
1966
2534
|
try {
|
|
@@ -1968,8 +2536,7 @@ async function runSkillsInstall() {
|
|
|
1968
2536
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1969
2537
|
manifest = await res.json();
|
|
1970
2538
|
} catch (err) {
|
|
1971
|
-
|
|
1972
|
-
process.stderr.write(`Failed to fetch manifest: ${message}
|
|
2539
|
+
process.stderr.write(`Failed to fetch manifest: ${safeRequestError(err)}
|
|
1973
2540
|
`);
|
|
1974
2541
|
process.exitCode = 1;
|
|
1975
2542
|
return;
|
|
@@ -1988,10 +2555,22 @@ async function runSkillsInstall() {
|
|
|
1988
2555
|
let installed = 0;
|
|
1989
2556
|
let failed = 0;
|
|
1990
2557
|
for (const skill of selected) {
|
|
1991
|
-
|
|
2558
|
+
if (!isSafeSkillSlug(skill.slug)) {
|
|
2559
|
+
process.stderr.write(" \u2717 invalid skill slug\n");
|
|
2560
|
+
failed++;
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
const skillDir = resolve(installDir, skill.slug);
|
|
2564
|
+
const relativeDestination = relative(installDir, skillDir);
|
|
2565
|
+
if (!relativeDestination || relativeDestination === ".." || relativeDestination.startsWith(`..${sep}`) || isAbsolute(relativeDestination)) {
|
|
2566
|
+
process.stderr.write(` \u2717 ${skill.slug}: invalid install destination
|
|
2567
|
+
`);
|
|
2568
|
+
failed++;
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
1992
2571
|
const skillPath = join(skillDir, "SKILL.md");
|
|
1993
|
-
const url = new URL(skill.contentUrl, source).toString();
|
|
1994
2572
|
try {
|
|
2573
|
+
const url = new URL(skill.contentUrl, source).toString();
|
|
1995
2574
|
const res = await fetch(url);
|
|
1996
2575
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1997
2576
|
const content = await res.text();
|
|
@@ -2001,8 +2580,7 @@ async function runSkillsInstall() {
|
|
|
2001
2580
|
`);
|
|
2002
2581
|
installed++;
|
|
2003
2582
|
} catch (err) {
|
|
2004
|
-
|
|
2005
|
-
process.stderr.write(` \u2717 ${skill.slug}: ${message}
|
|
2583
|
+
process.stderr.write(` \u2717 ${skill.slug}: ${safeRequestError(err)}
|
|
2006
2584
|
`);
|
|
2007
2585
|
failed++;
|
|
2008
2586
|
}
|
|
@@ -2012,6 +2590,23 @@ ${installed} installed, ${failed} failed
|
|
|
2012
2590
|
`);
|
|
2013
2591
|
process.stdout.write(`Location: ${targetDir}/
|
|
2014
2592
|
`);
|
|
2593
|
+
if (failed > 0) process.exitCode = 1;
|
|
2594
|
+
}
|
|
2595
|
+
function safeUrlForDisplay(value) {
|
|
2596
|
+
try {
|
|
2597
|
+
const url = new URL(value);
|
|
2598
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "custom source";
|
|
2599
|
+
url.username = "";
|
|
2600
|
+
url.password = "";
|
|
2601
|
+
url.search = "";
|
|
2602
|
+
url.hash = "";
|
|
2603
|
+
return url.toString();
|
|
2604
|
+
} catch {
|
|
2605
|
+
return "custom source";
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
function safeRequestError(error) {
|
|
2609
|
+
return error instanceof Error && /^HTTP \d{3}$/.test(error.message) ? error.message : "request failed";
|
|
2015
2610
|
}
|
|
2016
2611
|
async function selectSkills(skills, flags) {
|
|
2017
2612
|
if (flags.all === "true") return skills;
|
|
@@ -2138,3 +2733,47 @@ function parseNumberFlag(value) {
|
|
|
2138
2733
|
const parsed = Number(value);
|
|
2139
2734
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2140
2735
|
}
|
|
2736
|
+
async function runEnvCheck() {
|
|
2737
|
+
const { readFileSync: readFileSync3, existsSync: existsSync2 } = await import('fs');
|
|
2738
|
+
const { checkEnv: checkEnv2, formatEnvReport: formatEnvReport2 } = await Promise.resolve().then(() => (init_env_check(), env_check_exports));
|
|
2739
|
+
const flags = parseFlags(process.argv.slice(3));
|
|
2740
|
+
const examplePath = flags.example ?? ".env.example";
|
|
2741
|
+
if (!existsSync2(examplePath)) {
|
|
2742
|
+
process.stderr.write(
|
|
2743
|
+
`No example file at ${examplePath}.${BREAK}It is the schema this compares against; pass --example to point elsewhere.` + BREAK
|
|
2744
|
+
);
|
|
2745
|
+
return 1;
|
|
2746
|
+
}
|
|
2747
|
+
const example = readFileSync3(examplePath, "utf8");
|
|
2748
|
+
let actual;
|
|
2749
|
+
if (flags.file) {
|
|
2750
|
+
if (!existsSync2(flags.file)) {
|
|
2751
|
+
process.stderr.write(`No environment file at ${flags.file}.${BREAK}`);
|
|
2752
|
+
return 1;
|
|
2753
|
+
}
|
|
2754
|
+
actual = readFileSync3(flags.file, "utf8");
|
|
2755
|
+
} else {
|
|
2756
|
+
if (stdin.isTTY) {
|
|
2757
|
+
process.stdout.write(
|
|
2758
|
+
`Paste the environment below, then press ${process.platform === "win32" ? "Ctrl+Z and Enter" : "Ctrl+D"}.${BREAK}Nothing is stored or transmitted.` + BREAK + BREAK
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
actual = await readAllStdin();
|
|
2762
|
+
if (actual.trim() === "") {
|
|
2763
|
+
process.stderr.write("Nothing to check. Pass --file <path> or paste an environment." + BREAK);
|
|
2764
|
+
return 1;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
const result = checkEnv2({
|
|
2768
|
+
example,
|
|
2769
|
+
actual,
|
|
2770
|
+
optional: flags.optional ? flags.optional.split(",").map((key) => key.trim()).filter(Boolean) : []
|
|
2771
|
+
});
|
|
2772
|
+
process.stdout.write(`${formatEnvReport2(result)}${BREAK}`);
|
|
2773
|
+
return result.findings.length > 0 ? 1 : 0;
|
|
2774
|
+
}
|
|
2775
|
+
async function readAllStdin() {
|
|
2776
|
+
const chunks = [];
|
|
2777
|
+
for await (const chunk of stdin) chunks.push(Buffer.from(chunk));
|
|
2778
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2779
|
+
}
|