@litlab/audx 0.0.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -53
- package/dist/bin.js +1212 -0
- package/dist/cc-DgCkkqq8.js +13 -0
- package/dist/cc-he3fHS3P.js +12 -0
- package/dist/index.d.ts +723 -3
- package/dist/index.js +1534 -126
- package/dist/react.d.ts +583 -0
- package/dist/react.js +1556 -0
- package/package.json +64 -39
- package/schemas/pack.schema.json +4 -0
- package/schemas/theme.schema.json +857 -0
- package/dist/codegen/theme-codegen.d.ts +0 -12
- package/dist/codegen/theme-codegen.d.ts.map +0 -1
- package/dist/codegen/theme-codegen.js +0 -153
- package/dist/codegen/theme-codegen.js.map +0 -1
- package/dist/commands/add.d.ts +0 -2
- package/dist/commands/add.d.ts.map +0 -1
- package/dist/commands/add.js +0 -120
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/diff.d.ts +0 -2
- package/dist/commands/diff.d.ts.map +0 -1
- package/dist/commands/diff.js +0 -103
- package/dist/commands/diff.js.map +0 -1
- package/dist/commands/generate.d.ts +0 -12
- package/dist/commands/generate.d.ts.map +0 -1
- package/dist/commands/generate.js +0 -96
- package/dist/commands/generate.js.map +0 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -79
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/list.d.ts +0 -14
- package/dist/commands/list.d.ts.map +0 -1
- package/dist/commands/list.js +0 -93
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/remove.d.ts +0 -2
- package/dist/commands/remove.d.ts.map +0 -1
- package/dist/commands/remove.js +0 -71
- package/dist/commands/remove.js.map +0 -1
- package/dist/commands/theme.d.ts +0 -31
- package/dist/commands/theme.d.ts.map +0 -1
- package/dist/commands/theme.js +0 -142
- package/dist/commands/theme.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -123
- package/dist/commands/update.js.map +0 -1
- package/dist/core/alias-resolver.d.ts +0 -24
- package/dist/core/alias-resolver.d.ts.map +0 -1
- package/dist/core/alias-resolver.js +0 -87
- package/dist/core/alias-resolver.js.map +0 -1
- package/dist/core/config.d.ts +0 -21
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js +0 -43
- package/dist/core/config.js.map +0 -1
- package/dist/core/file-writer.d.ts +0 -14
- package/dist/core/file-writer.d.ts.map +0 -1
- package/dist/core/file-writer.js +0 -90
- package/dist/core/file-writer.js.map +0 -1
- package/dist/core/package-manager.d.ts +0 -3
- package/dist/core/package-manager.d.ts.map +0 -1
- package/dist/core/package-manager.js +0 -17
- package/dist/core/package-manager.js.map +0 -1
- package/dist/core/registry.d.ts +0 -18
- package/dist/core/registry.d.ts.map +0 -1
- package/dist/core/registry.js +0 -69
- package/dist/core/registry.js.map +0 -1
- package/dist/core/theme-manager.d.ts +0 -35
- package/dist/core/theme-manager.d.ts.map +0 -1
- package/dist/core/theme-manager.js +0 -94
- package/dist/core/theme-manager.js.map +0 -1
- package/dist/core/utils.d.ts +0 -22
- package/dist/core/utils.d.ts.map +0 -1
- package/dist/core/utils.js +0 -44
- package/dist/core/utils.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -121
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -198
- package/dist/types.js.map +0 -1
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { readdir, readFile, writeFile, unlink } from 'node:fs/promises';
|
|
5
|
+
import __node_cjsPath, { resolve, join, basename, isAbsolute } from 'node:path';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
import { _ as _object_without_properties_loose } from './cc-DgCkkqq8.js';
|
|
8
|
+
import __node_cjsModule from 'node:module';
|
|
9
|
+
import __node_cjsUrl from 'node:url';
|
|
10
|
+
|
|
11
|
+
const require$1 = __node_cjsModule.createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
const REGISTRY_BASE = "https://audx.site/api";
|
|
14
|
+
const CONFIG_DIR = ".themes";
|
|
15
|
+
const CONFIG_FILE = "config.json";
|
|
16
|
+
function configPath() {
|
|
17
|
+
return resolve(process.cwd(), CONFIG_DIR, CONFIG_FILE);
|
|
18
|
+
}
|
|
19
|
+
function getConfig() {
|
|
20
|
+
const p = configPath();
|
|
21
|
+
if (!existsSync(p)) return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(require$1("node:fs").readFileSync(p, "utf-8"));
|
|
24
|
+
if (typeof raw.output === "string") return {
|
|
25
|
+
output: raw.output
|
|
26
|
+
};
|
|
27
|
+
} catch (unused) {}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
async function ensureConfig(context = "setup") {
|
|
31
|
+
const existing = getConfig();
|
|
32
|
+
if (existing) return existing;
|
|
33
|
+
const isThemeContext = context === "themes";
|
|
34
|
+
const output = await p.select({
|
|
35
|
+
message: isThemeContext ? "Where should themes be installed?" : "Where should AudX be set up?",
|
|
36
|
+
options: isThemeContext ? [
|
|
37
|
+
{
|
|
38
|
+
value: "src/audio",
|
|
39
|
+
label: "src/audio/themes",
|
|
40
|
+
hint: "default"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: "lib/audio",
|
|
44
|
+
label: "lib/audio/themes"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: "__custom__",
|
|
48
|
+
label: "Custom path..."
|
|
49
|
+
}
|
|
50
|
+
] : [
|
|
51
|
+
{
|
|
52
|
+
value: "src/audio",
|
|
53
|
+
label: "src/audio",
|
|
54
|
+
hint: "default"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: "lib/audio",
|
|
58
|
+
label: "lib/audio"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
value: "__custom__",
|
|
62
|
+
label: "Custom path..."
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
if (p.isCancel(output)) {
|
|
67
|
+
p.cancel("Cancelled.");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
let outputDir = output;
|
|
71
|
+
if (outputDir === "__custom__") {
|
|
72
|
+
const custom = await p.text({
|
|
73
|
+
message: "Enter output path",
|
|
74
|
+
placeholder: isThemeContext ? "src/audio/themes" : "src/audio",
|
|
75
|
+
validate: (v)=>v.length === 0 ? "Path is required" : undefined
|
|
76
|
+
});
|
|
77
|
+
if (p.isCancel(custom)) {
|
|
78
|
+
p.cancel("Cancelled.");
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
outputDir = normalizeConfigOutput(custom, context);
|
|
82
|
+
}
|
|
83
|
+
const config = {
|
|
84
|
+
output: outputDir
|
|
85
|
+
};
|
|
86
|
+
const dir = resolve(process.cwd(), CONFIG_DIR);
|
|
87
|
+
if (!existsSync(dir)) mkdirSync(dir, {
|
|
88
|
+
recursive: true
|
|
89
|
+
});
|
|
90
|
+
require$1("node:fs").writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
91
|
+
return config;
|
|
92
|
+
}
|
|
93
|
+
function normalizeConfigOutput(output, context) {
|
|
94
|
+
const normalized = output.replace(/\/+$/, "");
|
|
95
|
+
if (context === "themes" && normalized.endsWith("/themes")) {
|
|
96
|
+
return normalized.slice(0, -"/themes".length);
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
function getThemesDir() {
|
|
101
|
+
var _ref;
|
|
102
|
+
const config = getConfig();
|
|
103
|
+
const output = (_ref = config == null ? void 0 : config.output) != null ? _ref : "src/audio";
|
|
104
|
+
return resolve(process.cwd(), output, "themes");
|
|
105
|
+
}
|
|
106
|
+
function getSoundsDir() {
|
|
107
|
+
var _ref;
|
|
108
|
+
const config = getConfig();
|
|
109
|
+
const output = (_ref = config == null ? void 0 : config.output) != null ? _ref : "src/audio";
|
|
110
|
+
return resolve(process.cwd(), output, "a");
|
|
111
|
+
}
|
|
112
|
+
function parseGitHubSource(source) {
|
|
113
|
+
const normalizedSource = source.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
114
|
+
const ghUrlMatch = normalizedSource.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\/tree\/([^/]+)(?:\/(.+))?)?$/);
|
|
115
|
+
if (ghUrlMatch) {
|
|
116
|
+
var _ghUrlMatch_, _ghUrlMatch_1;
|
|
117
|
+
return {
|
|
118
|
+
owner: ghUrlMatch[1],
|
|
119
|
+
repo: ghUrlMatch[2],
|
|
120
|
+
branch: (_ghUrlMatch_ = ghUrlMatch[3]) != null ? _ghUrlMatch_ : "main",
|
|
121
|
+
path: (_ghUrlMatch_1 = ghUrlMatch[4]) != null ? _ghUrlMatch_1 : ""
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const shorthand = normalizedSource.match(/^([^/]+)\/([^/]+)$/);
|
|
125
|
+
if (shorthand) {
|
|
126
|
+
return {
|
|
127
|
+
owner: shorthand[1],
|
|
128
|
+
repo: shorthand[2],
|
|
129
|
+
branch: "main",
|
|
130
|
+
path: ""
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
async function discoverThemesFromGitHub(source) {
|
|
136
|
+
const parsed = parseGitHubSource(source);
|
|
137
|
+
if (!parsed) {
|
|
138
|
+
throw new Error(`Invalid source: ${source}. Use owner/repo or a GitHub URL.`);
|
|
139
|
+
}
|
|
140
|
+
const { owner, repo, branch, path: subPath } = parsed;
|
|
141
|
+
const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
142
|
+
const res = await fetch(treeUrl, {
|
|
143
|
+
headers: {
|
|
144
|
+
Accept: "application/vnd.github.v3+json"
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
throw new Error(`Failed to fetch repo tree: ${res.status}`);
|
|
149
|
+
}
|
|
150
|
+
const tree = await res.json();
|
|
151
|
+
const jsonFiles = tree.tree.filter((item)=>{
|
|
152
|
+
if (item.type !== "blob" || !item.path.endsWith(".json")) return false;
|
|
153
|
+
if (subPath && !item.path.startsWith(subPath)) return false;
|
|
154
|
+
if (item.path.includes("node_modules/")) return false;
|
|
155
|
+
if (item.path.includes("package.json")) return false;
|
|
156
|
+
if (item.path.includes("tsconfig")) return false;
|
|
157
|
+
if (item.path.includes(".changeset/")) return false;
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
const themes = [];
|
|
161
|
+
for (const file of jsonFiles){
|
|
162
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${file.path}`;
|
|
163
|
+
try {
|
|
164
|
+
const r = await fetch(rawUrl);
|
|
165
|
+
if (!r.ok) continue;
|
|
166
|
+
const data = await r.json();
|
|
167
|
+
if (!validateTheme(data)) continue;
|
|
168
|
+
themes.push({
|
|
169
|
+
name: data.name,
|
|
170
|
+
path: file.path,
|
|
171
|
+
downloadUrl: rawUrl,
|
|
172
|
+
description: data.description,
|
|
173
|
+
soundCount: Object.keys(data.sounds).length
|
|
174
|
+
});
|
|
175
|
+
} catch (unused) {}
|
|
176
|
+
}
|
|
177
|
+
return themes;
|
|
178
|
+
}
|
|
179
|
+
function isGitHubSource(source) {
|
|
180
|
+
return parseGitHubSource(source) !== null;
|
|
181
|
+
}
|
|
182
|
+
function isLocalSource(source) {
|
|
183
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const abs = isAbsolute(source) ? source : resolve(process.cwd(), source);
|
|
187
|
+
return existsSync(abs);
|
|
188
|
+
}
|
|
189
|
+
async function discoverThemesFromLocal(source) {
|
|
190
|
+
const abs = isAbsolute(source) ? source : resolve(process.cwd(), source);
|
|
191
|
+
if (abs.endsWith(".json")) {
|
|
192
|
+
const raw = await readFile(abs, "utf-8");
|
|
193
|
+
const data = JSON.parse(raw);
|
|
194
|
+
if (!validateTheme(data)) {
|
|
195
|
+
throw new Error(`${source} is not a valid sound theme.`);
|
|
196
|
+
}
|
|
197
|
+
return [
|
|
198
|
+
{
|
|
199
|
+
name: data.name,
|
|
200
|
+
path: abs,
|
|
201
|
+
downloadUrl: abs,
|
|
202
|
+
description: data.description,
|
|
203
|
+
soundCount: Object.keys(data.sounds).length
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
const files = await readdir(abs);
|
|
208
|
+
const themes = [];
|
|
209
|
+
for (const file of files){
|
|
210
|
+
if (!file.endsWith(".json") || file === "index.json") continue;
|
|
211
|
+
try {
|
|
212
|
+
const filePath = join(abs, file);
|
|
213
|
+
const raw = await readFile(filePath, "utf-8");
|
|
214
|
+
const data = JSON.parse(raw);
|
|
215
|
+
if (!validateTheme(data)) continue;
|
|
216
|
+
themes.push({
|
|
217
|
+
name: data.name,
|
|
218
|
+
path: filePath,
|
|
219
|
+
downloadUrl: filePath,
|
|
220
|
+
description: data.description,
|
|
221
|
+
soundCount: Object.keys(data.sounds).length
|
|
222
|
+
});
|
|
223
|
+
} catch (unused) {}
|
|
224
|
+
}
|
|
225
|
+
return themes;
|
|
226
|
+
}
|
|
227
|
+
async function fetchThemeIndex() {
|
|
228
|
+
const res = await fetch(`${REGISTRY_BASE}/audio/themes`);
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
throw new Error(`Failed to fetch theme index: ${res.status}`);
|
|
231
|
+
}
|
|
232
|
+
return res.json();
|
|
233
|
+
}
|
|
234
|
+
async function fetchThemeJson(nameOrUrl) {
|
|
235
|
+
const url = nameOrUrl.startsWith("http") ? nameOrUrl : `${REGISTRY_BASE}/audio/theme/${nameOrUrl}`;
|
|
236
|
+
const res = await fetch(url);
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
throw new Error(`Failed to fetch theme: ${res.status}`);
|
|
239
|
+
}
|
|
240
|
+
return res.json();
|
|
241
|
+
}
|
|
242
|
+
async function registerTheme(url) {
|
|
243
|
+
try {
|
|
244
|
+
await fetch(`${REGISTRY_BASE}/audio/themes`, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: {
|
|
247
|
+
"Content-Type": "application/json"
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify({
|
|
250
|
+
url
|
|
251
|
+
})
|
|
252
|
+
});
|
|
253
|
+
} catch (unused) {}
|
|
254
|
+
}
|
|
255
|
+
function validateTheme(data) {
|
|
256
|
+
return typeof data.name === "string" && typeof data.sounds === "object" && data.sounds !== null;
|
|
257
|
+
}
|
|
258
|
+
async function getInstalledThemes() {
|
|
259
|
+
const dir = getThemesDir();
|
|
260
|
+
if (!existsSync(dir)) return [];
|
|
261
|
+
const files = await readdir(dir);
|
|
262
|
+
const themes = [];
|
|
263
|
+
for (const file of files){
|
|
264
|
+
if (!file.endsWith(".ts") || file === "index.ts") continue;
|
|
265
|
+
try {
|
|
266
|
+
var _raw_match, _ref;
|
|
267
|
+
const raw = await readFile(join(dir, file), "utf-8");
|
|
268
|
+
const nameMatch = raw.match(/^\/\/ theme: (.+)$/m);
|
|
269
|
+
const exportCount = ((_raw_match = raw.match(/^export const /gm)) != null ? _raw_match : []).length;
|
|
270
|
+
const name = (_ref = nameMatch == null ? void 0 : nameMatch[1]) != null ? _ref : basename(file, ".ts");
|
|
271
|
+
themes.push({
|
|
272
|
+
file,
|
|
273
|
+
name,
|
|
274
|
+
soundCount: Math.max(0, exportCount - 1)
|
|
275
|
+
});
|
|
276
|
+
} catch (unused) {}
|
|
277
|
+
}
|
|
278
|
+
return themes;
|
|
279
|
+
}
|
|
280
|
+
const RESERVED = new Set([
|
|
281
|
+
"break",
|
|
282
|
+
"case",
|
|
283
|
+
"catch",
|
|
284
|
+
"continue",
|
|
285
|
+
"debugger",
|
|
286
|
+
"default",
|
|
287
|
+
"delete",
|
|
288
|
+
"do",
|
|
289
|
+
"else",
|
|
290
|
+
"finally",
|
|
291
|
+
"for",
|
|
292
|
+
"function",
|
|
293
|
+
"if",
|
|
294
|
+
"in",
|
|
295
|
+
"instanceof",
|
|
296
|
+
"new",
|
|
297
|
+
"return",
|
|
298
|
+
"switch",
|
|
299
|
+
"this",
|
|
300
|
+
"throw",
|
|
301
|
+
"try",
|
|
302
|
+
"typeof",
|
|
303
|
+
"var",
|
|
304
|
+
"void",
|
|
305
|
+
"while",
|
|
306
|
+
"with",
|
|
307
|
+
"class",
|
|
308
|
+
"const",
|
|
309
|
+
"enum",
|
|
310
|
+
"export",
|
|
311
|
+
"extends",
|
|
312
|
+
"import",
|
|
313
|
+
"super",
|
|
314
|
+
"implements",
|
|
315
|
+
"interface",
|
|
316
|
+
"let",
|
|
317
|
+
"package",
|
|
318
|
+
"private",
|
|
319
|
+
"protected",
|
|
320
|
+
"public",
|
|
321
|
+
"static",
|
|
322
|
+
"yield",
|
|
323
|
+
"await"
|
|
324
|
+
]);
|
|
325
|
+
function toCamelCase(s) {
|
|
326
|
+
let id = s.replace(/[^a-zA-Z0-9]+(.)/g, (_, c)=>c.toUpperCase()).replace(/^[^a-zA-Z_$]/, "_$&");
|
|
327
|
+
if (RESERVED.has(id)) id = `_${id}`;
|
|
328
|
+
return id;
|
|
329
|
+
}
|
|
330
|
+
function toIdentifier(s) {
|
|
331
|
+
return toCamelCase(s);
|
|
332
|
+
}
|
|
333
|
+
function generateModule(data) {
|
|
334
|
+
const entries = Object.entries(data.sounds);
|
|
335
|
+
const ids = entries.map(([key])=>toCamelCase(key));
|
|
336
|
+
const lines = [
|
|
337
|
+
`// ${data.name} — generated by @litlab/audx (do not edit)`,
|
|
338
|
+
`// theme: ${data.name}`,
|
|
339
|
+
`import type { SoundDefinition, SoundPatch } from "@litlab/audx";`,
|
|
340
|
+
""
|
|
341
|
+
];
|
|
342
|
+
for(let i = 0; i < entries.length; i++){
|
|
343
|
+
const [, def] = entries[i];
|
|
344
|
+
lines.push(`export const ${ids[i]}: SoundDefinition = ${JSON.stringify(def)};`);
|
|
345
|
+
}
|
|
346
|
+
const { sounds: _, $schema: _s } = data, meta = _object_without_properties_loose(data, [
|
|
347
|
+
"sounds",
|
|
348
|
+
"$schema"
|
|
349
|
+
]);
|
|
350
|
+
const soundsObj = ids.map((id, i)=>{
|
|
351
|
+
const key = entries[i][0];
|
|
352
|
+
return id === key ? id : `${JSON.stringify(key)}: ${id}`;
|
|
353
|
+
});
|
|
354
|
+
lines.push("", `export const _patch: SoundPatch = {`, ` ...${JSON.stringify(meta)},`, ` sounds: { ${soundsObj.join(", ")} },`, `};`, "");
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
357
|
+
function generateSoundModule(name, definition) {
|
|
358
|
+
const id = toCamelCase(name);
|
|
359
|
+
return [
|
|
360
|
+
`import { defineSound } from "@litlab/audx";`,
|
|
361
|
+
"",
|
|
362
|
+
`export const ${id} = defineSound(${JSON.stringify(definition, null, 2)});`,
|
|
363
|
+
""
|
|
364
|
+
].join("\n");
|
|
365
|
+
}
|
|
366
|
+
async function regenerateIndex(dir) {
|
|
367
|
+
if (!existsSync(dir)) return;
|
|
368
|
+
const files = await readdir(dir);
|
|
369
|
+
const modules = files.filter((f)=>f.endsWith(".ts") && f !== "index.ts").map((f)=>basename(f, ".ts")).sort();
|
|
370
|
+
const lines = [
|
|
371
|
+
"// generated by @litlab/audx (do not edit)",
|
|
372
|
+
...modules.map((m)=>`export * as ${toCamelCase(m)} from "./${m}";`),
|
|
373
|
+
""
|
|
374
|
+
];
|
|
375
|
+
await writeFile(join(dir, "index.ts"), lines.join("\n"), "utf-8");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function parseAddOptions(args) {
|
|
379
|
+
const options = {};
|
|
380
|
+
let source;
|
|
381
|
+
for(let i = 0; i < args.length; i++){
|
|
382
|
+
const arg = args[i];
|
|
383
|
+
if (arg === "-y" || arg === "--yes") {
|
|
384
|
+
options.yes = true;
|
|
385
|
+
} else if (arg === "-l" || arg === "--list") {
|
|
386
|
+
options.list = true;
|
|
387
|
+
} else if (arg === "--theme") {
|
|
388
|
+
options.theme = args[++i];
|
|
389
|
+
} else if (arg && !arg.startsWith("-")) {
|
|
390
|
+
source = arg;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
source,
|
|
395
|
+
options
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async function add(args) {
|
|
399
|
+
const { source, options } = parseAddOptions(args);
|
|
400
|
+
p.intro("@litlab/audx add");
|
|
401
|
+
if (!source) {
|
|
402
|
+
await addFromRegistry(options);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (isLocalSource(source)) {
|
|
406
|
+
await addFromLocal(source, options);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (source.startsWith("http") && source.endsWith(".json")) {
|
|
410
|
+
await addFromUrl(source, options);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (isGitHubSource(source)) {
|
|
414
|
+
await addFromGitHub(source, options);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
await addSoundFromRegistry(source, options);
|
|
418
|
+
}
|
|
419
|
+
async function addFromLocal(source, options) {
|
|
420
|
+
const s = p.spinner();
|
|
421
|
+
s.start("Scanning local path for themes...");
|
|
422
|
+
let discovered;
|
|
423
|
+
try {
|
|
424
|
+
discovered = await discoverThemesFromLocal(source);
|
|
425
|
+
s.stop(`Found ${discovered.length} theme(es)`);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
s.stop("Failed to scan local path.");
|
|
428
|
+
p.log.error(String(err));
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
if (discovered.length === 0) {
|
|
432
|
+
p.log.warn("No valid sound themes found at this path.");
|
|
433
|
+
p.outro("Themes must be JSON files with a name and sounds object.");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (options.list) {
|
|
437
|
+
printThemeList(discovered);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const toInstall = selectThemes(discovered, options);
|
|
441
|
+
if (!toInstall || toInstall.length === 0) return;
|
|
442
|
+
const installed = await getInstalledThemes();
|
|
443
|
+
const installedNames = new Set(installed.map((p)=>p.name));
|
|
444
|
+
const final = options.yes ? toInstall : await confirmThemeOverwrites(toInstall, installedNames);
|
|
445
|
+
if (!final || final.length === 0) return;
|
|
446
|
+
const dl = p.spinner();
|
|
447
|
+
dl.start(`Installing ${final.length} theme(es)...`);
|
|
448
|
+
const results = [];
|
|
449
|
+
for (const theme of final){
|
|
450
|
+
try {
|
|
451
|
+
const raw = await readFile(theme.downloadUrl, "utf-8");
|
|
452
|
+
const data = JSON.parse(raw);
|
|
453
|
+
if (!validateTheme(data)) {
|
|
454
|
+
p.log.warn(`Skipping ${theme.name}: invalid theme format`);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
await writeTheme(theme.name, data);
|
|
458
|
+
results.push(data.name);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
p.log.warn(`Failed to install ${theme.name}: ${err}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
dl.stop(`Installed ${results.length} theme(es)`);
|
|
464
|
+
p.note(results.map((n)=>` - ${n}`).join("\n"), "Installed themes");
|
|
465
|
+
p.outro("Done!");
|
|
466
|
+
}
|
|
467
|
+
async function addFromGitHub(source, options) {
|
|
468
|
+
const s = p.spinner();
|
|
469
|
+
s.start("Scanning repository for themes...");
|
|
470
|
+
let discovered;
|
|
471
|
+
try {
|
|
472
|
+
discovered = await discoverThemesFromGitHub(source);
|
|
473
|
+
s.stop(`Found ${discovered.length} theme(es)`);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
s.stop("Failed to scan repository.");
|
|
476
|
+
p.log.error(String(err));
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
if (discovered.length === 0) {
|
|
480
|
+
p.log.warn("No valid sound themes found in this repository.");
|
|
481
|
+
p.outro("Themes must be JSON files with a name and sounds object.");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (options.list) {
|
|
485
|
+
printThemeList(discovered);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const installed = await getInstalledThemes();
|
|
489
|
+
const installedNames = new Set(installed.map((p)=>p.name));
|
|
490
|
+
const toInstall = await resolveThemeSelection(discovered, installedNames, options);
|
|
491
|
+
if (!toInstall || toInstall.length === 0) return;
|
|
492
|
+
const dl = p.spinner();
|
|
493
|
+
dl.start(`Installing ${toInstall.length} theme(es)...`);
|
|
494
|
+
const results = [];
|
|
495
|
+
for (const theme of toInstall){
|
|
496
|
+
try {
|
|
497
|
+
const data = await fetchThemeJson(theme.downloadUrl);
|
|
498
|
+
if (!validateTheme(data)) {
|
|
499
|
+
p.log.warn(`Skipping ${theme.name}: invalid theme format`);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
await writeTheme(theme.name, data);
|
|
503
|
+
registerTheme(theme.downloadUrl);
|
|
504
|
+
results.push(data.name);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
p.log.warn(`Failed to install ${theme.name}: ${err}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
dl.stop(`Installed ${results.length} theme(es)`);
|
|
510
|
+
p.note(results.map((n)=>` - ${n}`).join("\n"), "Installed themes");
|
|
511
|
+
p.outro("Done!");
|
|
512
|
+
}
|
|
513
|
+
async function addFromUrl(url, options) {
|
|
514
|
+
const s = p.spinner();
|
|
515
|
+
s.start("Fetching theme...");
|
|
516
|
+
try {
|
|
517
|
+
const data = await fetchThemeJson(url);
|
|
518
|
+
if (!validateTheme(data)) {
|
|
519
|
+
s.stop("Invalid theme format.");
|
|
520
|
+
p.log.error("The fetched JSON is not a valid sound theme (missing name or sounds).");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
s.stop(`Fetched "${data.name}"`);
|
|
524
|
+
if (options.list) {
|
|
525
|
+
console.log(` ${pc.bold(data.name)} ${pc.dim(`${Object.keys(data.sounds).length} sounds`)}`);
|
|
526
|
+
console.log();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
await writeTheme(data.name, data);
|
|
530
|
+
registerTheme(url);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
s.stop("Failed to fetch theme.");
|
|
533
|
+
p.log.error(String(err));
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function addFromRegistry(options) {
|
|
538
|
+
const s = p.spinner();
|
|
539
|
+
s.start("Fetching available themes...");
|
|
540
|
+
let index;
|
|
541
|
+
try {
|
|
542
|
+
index = await fetchThemeIndex();
|
|
543
|
+
s.stop(`Found ${index.length} themes`);
|
|
544
|
+
} catch (err) {
|
|
545
|
+
s.stop("Failed to fetch theme index.");
|
|
546
|
+
p.log.error(String(err));
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
if (options.list) {
|
|
550
|
+
console.log();
|
|
551
|
+
for (const entry of index){
|
|
552
|
+
console.log(` ${pc.bold(entry.name)} ${pc.dim(entry.description)}`);
|
|
553
|
+
}
|
|
554
|
+
console.log();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const installed = await getInstalledThemes();
|
|
558
|
+
const installedNames = new Set(installed.map((p)=>p.name));
|
|
559
|
+
let names;
|
|
560
|
+
if (options.theme) {
|
|
561
|
+
const themeName = options.theme;
|
|
562
|
+
names = [
|
|
563
|
+
themeName
|
|
564
|
+
];
|
|
565
|
+
const match = index.find((e)=>e.name.toLowerCase() === themeName.toLowerCase());
|
|
566
|
+
if (!match) {
|
|
567
|
+
p.log.error(`Theme "${themeName}" not found in registry.`);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
} else if (options.yes) {
|
|
571
|
+
names = index.map((e)=>e.name);
|
|
572
|
+
} else {
|
|
573
|
+
const selected = await p.multiselect({
|
|
574
|
+
message: "Select themes to install",
|
|
575
|
+
options: index.map((entry)=>({
|
|
576
|
+
value: entry.name,
|
|
577
|
+
label: `${entry.name}${installedNames.has(entry.name) ? " (installed)" : ""}`,
|
|
578
|
+
hint: entry.description
|
|
579
|
+
}))
|
|
580
|
+
});
|
|
581
|
+
if (p.isCancel(selected)) {
|
|
582
|
+
p.cancel("Cancelled.");
|
|
583
|
+
process.exit(0);
|
|
584
|
+
}
|
|
585
|
+
names = selected;
|
|
586
|
+
if (names.length === 0) {
|
|
587
|
+
p.outro("No themes selected.");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (!options.yes) {
|
|
592
|
+
const existing = names.filter((n)=>installedNames.has(n));
|
|
593
|
+
if (existing.length > 0) {
|
|
594
|
+
const overwrite = await p.confirm({
|
|
595
|
+
message: `${existing.length} theme(es) already installed. Overwrite?`
|
|
596
|
+
});
|
|
597
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
598
|
+
p.cancel("Cancelled.");
|
|
599
|
+
process.exit(0);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const dl = p.spinner();
|
|
604
|
+
dl.start(`Downloading ${names.length} theme(es)...`);
|
|
605
|
+
const results = [];
|
|
606
|
+
for (const name of names){
|
|
607
|
+
try {
|
|
608
|
+
const data = await fetchThemeJson(name);
|
|
609
|
+
if (!validateTheme(data)) {
|
|
610
|
+
p.log.warn(`Skipping ${name}: invalid theme format`);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
await writeTheme(name, data);
|
|
614
|
+
results.push(data.name);
|
|
615
|
+
} catch (err) {
|
|
616
|
+
p.log.warn(`Failed to download ${name}: ${err}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
dl.stop(`Downloaded ${results.length} theme(es)`);
|
|
620
|
+
p.note(results.map((n)=>` - ${n}`).join("\n"), "Installed themes");
|
|
621
|
+
p.outro("Done!");
|
|
622
|
+
}
|
|
623
|
+
async function addSoundFromRegistry(soundName, options) {
|
|
624
|
+
if (options.list) {
|
|
625
|
+
p.log.error("--list is only available when browsing themes.");
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
const s = p.spinner();
|
|
629
|
+
s.start(`Finding "${soundName}"...`);
|
|
630
|
+
let index;
|
|
631
|
+
try {
|
|
632
|
+
index = await fetchThemeIndex();
|
|
633
|
+
} catch (err) {
|
|
634
|
+
s.stop("Failed to fetch theme index.");
|
|
635
|
+
p.log.error(String(err));
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
let found;
|
|
639
|
+
for (const entry of index){
|
|
640
|
+
try {
|
|
641
|
+
const data = await fetchThemeJson(entry.name);
|
|
642
|
+
if (!validateTheme(data)) continue;
|
|
643
|
+
const match = Object.entries(data.sounds).find(([name])=>name.toLowerCase() === soundName.toLowerCase());
|
|
644
|
+
if (match) {
|
|
645
|
+
found = {
|
|
646
|
+
theme: data.name,
|
|
647
|
+
definition: match[1]
|
|
648
|
+
};
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
} catch (unused) {}
|
|
652
|
+
}
|
|
653
|
+
if (!found) {
|
|
654
|
+
s.stop(`Sound "${soundName}" not found.`);
|
|
655
|
+
p.log.error("Run `@litlab/audx add` to browse available themes.");
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
s.stop(`Found "${soundName}" in ${found.theme}`);
|
|
659
|
+
await writeSound(soundName, found.definition, options);
|
|
660
|
+
p.note(` - ${soundName}`, "Installed sound");
|
|
661
|
+
p.outro("Done!");
|
|
662
|
+
}
|
|
663
|
+
function printThemeList(themes) {
|
|
664
|
+
console.log();
|
|
665
|
+
for (const theme of themes){
|
|
666
|
+
const desc = theme.description ? ` ${pc.dim(theme.description)}` : "";
|
|
667
|
+
console.log(` ${pc.bold(theme.name)} ${pc.dim(`${theme.soundCount} sounds`)}${desc}`);
|
|
668
|
+
}
|
|
669
|
+
console.log();
|
|
670
|
+
}
|
|
671
|
+
function selectThemes(discovered, options) {
|
|
672
|
+
if (options.theme) {
|
|
673
|
+
const themeName = options.theme;
|
|
674
|
+
const match = discovered.filter((d)=>d.name.toLowerCase() === themeName.toLowerCase());
|
|
675
|
+
if (match.length === 0) {
|
|
676
|
+
p.log.error(`Theme "${themeName}" not found.`);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
return match;
|
|
680
|
+
}
|
|
681
|
+
if (options.yes) return discovered;
|
|
682
|
+
return discovered;
|
|
683
|
+
}
|
|
684
|
+
async function resolveThemeSelection(discovered, installedNames, options) {
|
|
685
|
+
if (options.theme) {
|
|
686
|
+
const themeName = options.theme;
|
|
687
|
+
const match = discovered.filter((d)=>d.name.toLowerCase() === themeName.toLowerCase());
|
|
688
|
+
if (match.length === 0) {
|
|
689
|
+
p.log.error(`Theme "${themeName}" not found.`);
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
return match;
|
|
693
|
+
}
|
|
694
|
+
if (options.yes) return discovered;
|
|
695
|
+
if (discovered.length === 1) return discovered;
|
|
696
|
+
return await promptThemeSelection(discovered, installedNames);
|
|
697
|
+
}
|
|
698
|
+
async function promptThemeSelection(discovered, installedNames) {
|
|
699
|
+
const selected = await p.multiselect({
|
|
700
|
+
message: "Select themes to install",
|
|
701
|
+
options: discovered.map((theme)=>({
|
|
702
|
+
value: theme.name,
|
|
703
|
+
label: `${theme.name}${installedNames.has(theme.name) ? " (installed)" : ""}`,
|
|
704
|
+
hint: theme.description ? `${theme.soundCount} sounds — ${theme.description}` : `${theme.soundCount} sounds`
|
|
705
|
+
}))
|
|
706
|
+
});
|
|
707
|
+
if (p.isCancel(selected)) {
|
|
708
|
+
p.cancel("Cancelled.");
|
|
709
|
+
process.exit(0);
|
|
710
|
+
}
|
|
711
|
+
const names = new Set(selected);
|
|
712
|
+
return discovered.filter((d)=>names.has(d.name));
|
|
713
|
+
}
|
|
714
|
+
async function confirmThemeOverwrites(themes, installedNames) {
|
|
715
|
+
const existing = themes.filter((theme)=>installedNames.has(theme.name));
|
|
716
|
+
if (existing.length === 0) return themes;
|
|
717
|
+
const overwrite = await p.confirm({
|
|
718
|
+
message: `${existing.length} theme(es) already installed. Overwrite?`
|
|
719
|
+
});
|
|
720
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
721
|
+
p.cancel("Cancelled.");
|
|
722
|
+
process.exit(0);
|
|
723
|
+
}
|
|
724
|
+
return themes;
|
|
725
|
+
}
|
|
726
|
+
async function writeTheme(filename, data) {
|
|
727
|
+
await ensureConfig("themes");
|
|
728
|
+
const dir = getThemesDir();
|
|
729
|
+
if (!existsSync(dir)) {
|
|
730
|
+
mkdirSync(dir, {
|
|
731
|
+
recursive: true
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
const slug = filename.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
735
|
+
const moduleSource = generateModule(data);
|
|
736
|
+
const target = join(dir, `${slug}.ts`);
|
|
737
|
+
await writeFile(target, moduleSource, "utf-8");
|
|
738
|
+
await regenerateIndex(dir);
|
|
739
|
+
}
|
|
740
|
+
async function writeSound(name, definition, options) {
|
|
741
|
+
await ensureConfig("setup");
|
|
742
|
+
const dir = getSoundsDir();
|
|
743
|
+
if (!existsSync(dir)) {
|
|
744
|
+
mkdirSync(dir, {
|
|
745
|
+
recursive: true
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
749
|
+
const target = join(dir, `${slug}.ts`);
|
|
750
|
+
if (existsSync(target) && !options.yes) {
|
|
751
|
+
const overwrite = await p.confirm({
|
|
752
|
+
message: `${slug}.ts already exists. Overwrite?`
|
|
753
|
+
});
|
|
754
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
755
|
+
p.cancel("Cancelled.");
|
|
756
|
+
process.exit(0);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
await writeFile(target, generateSoundModule(toIdentifier(name), definition), "utf-8");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function check(_args) {
|
|
763
|
+
p.intro("@litlab/audx check");
|
|
764
|
+
const installed = await getInstalledThemes();
|
|
765
|
+
if (installed.length === 0) {
|
|
766
|
+
p.log.warn("No themes installed.");
|
|
767
|
+
p.outro("Install themes with npx @litlab/audx add");
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const s = p.spinner();
|
|
771
|
+
s.start("Checking for updates...");
|
|
772
|
+
let registry;
|
|
773
|
+
try {
|
|
774
|
+
registry = await fetchThemeIndex();
|
|
775
|
+
s.stop(`Checked ${registry.length} registry theme(es)`);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
s.stop("Failed to fetch registry.");
|
|
778
|
+
p.log.error(String(err));
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
const registryMap = new Map(registry.map((e)=>[
|
|
782
|
+
e.name.toLowerCase(),
|
|
783
|
+
e
|
|
784
|
+
]));
|
|
785
|
+
const available = [];
|
|
786
|
+
const notInRegistry = [];
|
|
787
|
+
for (const entry of installed){
|
|
788
|
+
const regEntry = registryMap.get(entry.name.toLowerCase());
|
|
789
|
+
if (regEntry) {
|
|
790
|
+
available.push(entry.name);
|
|
791
|
+
} else {
|
|
792
|
+
notInRegistry.push(entry.name);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (available.length === 0) {
|
|
796
|
+
p.log.warn("No installed themes found in the registry.");
|
|
797
|
+
p.outro("");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
p.note(available.map((name)=>` ↑ ${name}`).join("\n"), `${available.length} theme(es) available`);
|
|
801
|
+
if (notInRegistry.length > 0) {
|
|
802
|
+
p.log.warn([
|
|
803
|
+
`${notInRegistry.length} theme(es) not found in registry:`,
|
|
804
|
+
...notInRegistry.map((name)=>` • ${name}`)
|
|
805
|
+
].join("\n"));
|
|
806
|
+
}
|
|
807
|
+
p.outro("Run npx @litlab/audx update to re-download latest versions");
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function find(args) {
|
|
811
|
+
const query = args.join(" ").toLowerCase();
|
|
812
|
+
p.intro("@litlab/audx find");
|
|
813
|
+
const s = p.spinner();
|
|
814
|
+
s.start("Fetching registry...");
|
|
815
|
+
let index;
|
|
816
|
+
try {
|
|
817
|
+
index = await fetchThemeIndex();
|
|
818
|
+
s.stop(`Found ${index.length} theme(es) in registry`);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
s.stop("Failed to fetch registry.");
|
|
821
|
+
p.log.error(String(err));
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
if (index.length === 0) {
|
|
825
|
+
p.log.warn("No themes available in the registry.");
|
|
826
|
+
p.outro("");
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const matches = query ? index.filter((entry)=>{
|
|
830
|
+
var _entry_tags;
|
|
831
|
+
const haystack = `${entry.name} ${entry.description} ${((_entry_tags = entry.tags) != null ? _entry_tags : []).join(" ")}`.toLowerCase();
|
|
832
|
+
return haystack.includes(query);
|
|
833
|
+
}) : index;
|
|
834
|
+
if (matches.length === 0) {
|
|
835
|
+
p.log.warn(`No themes found for "${query}"`);
|
|
836
|
+
p.outro("");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
p.log.info("Install with npx @litlab/audx add --theme <name>");
|
|
840
|
+
for (const entry of matches){
|
|
841
|
+
const tags = entry.tags && entry.tags.length > 0 ? ` ${entry.tags.join(", ")}` : "";
|
|
842
|
+
const desc = entry.description ? `\n ${entry.description}` : "";
|
|
843
|
+
p.log.step(`${entry.name}${tags}${desc}`);
|
|
844
|
+
}
|
|
845
|
+
p.outro(`${matches.length} result(s)`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function init(args) {
|
|
849
|
+
if (args[0] === "theme") {
|
|
850
|
+
await themeInit(args.slice(1));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
p.intro("@litlab/audx init");
|
|
854
|
+
await ensureConfig();
|
|
855
|
+
await add([]);
|
|
856
|
+
}
|
|
857
|
+
async function themeInit(_args) {
|
|
858
|
+
p.intro("@litlab/audx theme init");
|
|
859
|
+
const name = await p.text({
|
|
860
|
+
message: "Theme name",
|
|
861
|
+
placeholder: "my-theme",
|
|
862
|
+
validate: (v)=>v.length === 0 ? "Name is required" : undefined
|
|
863
|
+
});
|
|
864
|
+
if (p.isCancel(name)) {
|
|
865
|
+
p.cancel("Cancelled.");
|
|
866
|
+
process.exit(0);
|
|
867
|
+
}
|
|
868
|
+
const author = await p.text({
|
|
869
|
+
message: "Author",
|
|
870
|
+
placeholder: "Your name"
|
|
871
|
+
});
|
|
872
|
+
if (p.isCancel(author)) {
|
|
873
|
+
p.cancel("Cancelled.");
|
|
874
|
+
process.exit(0);
|
|
875
|
+
}
|
|
876
|
+
const description = await p.text({
|
|
877
|
+
message: "Description",
|
|
878
|
+
placeholder: "What does this theme sound like?"
|
|
879
|
+
});
|
|
880
|
+
if (p.isCancel(description)) {
|
|
881
|
+
p.cancel("Cancelled.");
|
|
882
|
+
process.exit(0);
|
|
883
|
+
}
|
|
884
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
885
|
+
const filename = `${slug}.json`;
|
|
886
|
+
const dir = resolve(process.cwd(), ".audx", "themes");
|
|
887
|
+
if (!existsSync(dir)) {
|
|
888
|
+
mkdirSync(dir, {
|
|
889
|
+
recursive: true
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const target = resolve(dir, filename);
|
|
893
|
+
if (existsSync(target)) {
|
|
894
|
+
const overwrite = await p.confirm({
|
|
895
|
+
message: `${filename} already exists. Overwrite?`
|
|
896
|
+
});
|
|
897
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
898
|
+
p.cancel("Cancelled.");
|
|
899
|
+
process.exit(0);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const theme = {
|
|
903
|
+
$schema: "../../node_modules/@litlab/audx/schemas/theme.schema.json",
|
|
904
|
+
name: name,
|
|
905
|
+
author: author || undefined,
|
|
906
|
+
version: "1.0.0",
|
|
907
|
+
description: description || undefined,
|
|
908
|
+
tags: [],
|
|
909
|
+
sounds: {}
|
|
910
|
+
};
|
|
911
|
+
await writeFile(target, `${JSON.stringify(theme, null, 2)}\n`, "utf-8");
|
|
912
|
+
p.log.success(`Created .audx/themes/${filename}`);
|
|
913
|
+
p.outro("Add sounds to the `sounds` object to get started.");
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async function list(_args) {
|
|
917
|
+
p.intro("@litlab/audx list");
|
|
918
|
+
const themes = await getInstalledThemes();
|
|
919
|
+
if (themes.length === 0) {
|
|
920
|
+
p.log.warn(`No themes found in ${getThemesDir()}`);
|
|
921
|
+
p.outro("Run `@litlab/audx add` to install themes.");
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const rows = themes.map((theme)=>{
|
|
925
|
+
var _theme_description;
|
|
926
|
+
return ` ${theme.name.padEnd(16)} ${String(theme.soundCount).padStart(3)} sounds ${(_theme_description = theme.description) != null ? _theme_description : ""}`;
|
|
927
|
+
});
|
|
928
|
+
p.note(rows.join("\n"), `${themes.length} theme(es) installed`);
|
|
929
|
+
p.outro(getThemesDir());
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function parseRemoveOptions(args) {
|
|
933
|
+
const options = {};
|
|
934
|
+
const themes = [];
|
|
935
|
+
for(let i = 0; i < args.length; i++){
|
|
936
|
+
const arg = args[i];
|
|
937
|
+
if (arg === "-y" || arg === "--yes") {
|
|
938
|
+
options.yes = true;
|
|
939
|
+
} else if (arg && !arg.startsWith("-")) {
|
|
940
|
+
themes.push(arg);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
themes,
|
|
945
|
+
options
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
async function remove(args) {
|
|
949
|
+
const { themes: themeNames, options } = parseRemoveOptions(args);
|
|
950
|
+
p.intro("@litlab/audx remove");
|
|
951
|
+
const themes = await getInstalledThemes();
|
|
952
|
+
if (themes.length === 0) {
|
|
953
|
+
p.log.warn("No themes installed.");
|
|
954
|
+
p.outro("Nothing to remove.");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
let files;
|
|
958
|
+
if (themeNames.length > 0) {
|
|
959
|
+
const matched = themes.filter((pk)=>themeNames.some((n)=>n.toLowerCase() === pk.name.toLowerCase()));
|
|
960
|
+
if (matched.length === 0) {
|
|
961
|
+
p.log.error(`No matching themes found for: ${themeNames.join(", ")}`);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
files = matched.map((pk)=>pk.file);
|
|
965
|
+
} else {
|
|
966
|
+
const selected = await p.multiselect({
|
|
967
|
+
message: "Select themes to remove",
|
|
968
|
+
options: themes.map((pk)=>({
|
|
969
|
+
value: pk.file,
|
|
970
|
+
label: pk.name,
|
|
971
|
+
hint: `${pk.soundCount} sounds`
|
|
972
|
+
}))
|
|
973
|
+
});
|
|
974
|
+
if (p.isCancel(selected)) {
|
|
975
|
+
p.cancel("Cancelled.");
|
|
976
|
+
process.exit(0);
|
|
977
|
+
}
|
|
978
|
+
files = selected;
|
|
979
|
+
if (files.length === 0) {
|
|
980
|
+
p.outro("No themes selected.");
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (!options.yes) {
|
|
985
|
+
const confirmed = await p.confirm({
|
|
986
|
+
message: `Remove ${files.length} theme(es)?`
|
|
987
|
+
});
|
|
988
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
989
|
+
p.cancel("Cancelled.");
|
|
990
|
+
process.exit(0);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const dir = getThemesDir();
|
|
994
|
+
const removed = [];
|
|
995
|
+
for (const file of files){
|
|
996
|
+
try {
|
|
997
|
+
var _ref;
|
|
998
|
+
await unlink(join(dir, file));
|
|
999
|
+
const pk = themes.find((item)=>item.file === file);
|
|
1000
|
+
removed.push((_ref = pk == null ? void 0 : pk.name) != null ? _ref : file);
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
p.log.warn(`Failed to remove ${file}: ${err}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
await regenerateIndex(dir);
|
|
1006
|
+
p.note(removed.map((n)=>` - ${n}`).join("\n"), "Removed themes");
|
|
1007
|
+
p.outro("Done!");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
async function update(_args) {
|
|
1011
|
+
p.intro("@litlab/audx update");
|
|
1012
|
+
const installed = await getInstalledThemes();
|
|
1013
|
+
if (installed.length === 0) {
|
|
1014
|
+
p.log.warn("No themes installed.");
|
|
1015
|
+
p.outro("Install themes with npx @litlab/audx add");
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const s = p.spinner();
|
|
1019
|
+
s.start("Fetching registry...");
|
|
1020
|
+
let registry;
|
|
1021
|
+
try {
|
|
1022
|
+
registry = await fetchThemeIndex();
|
|
1023
|
+
s.stop(`Found ${registry.length} registry theme(es)`);
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
s.stop("Failed to fetch registry.");
|
|
1026
|
+
p.log.error(String(err));
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
const registryMap = new Map(registry.map((e)=>[
|
|
1030
|
+
e.name.toLowerCase(),
|
|
1031
|
+
e
|
|
1032
|
+
]));
|
|
1033
|
+
const toUpdate = installed.filter((pk)=>registryMap.has(pk.name.toLowerCase()));
|
|
1034
|
+
if (toUpdate.length === 0) {
|
|
1035
|
+
p.log.warn("No installed themes found in the registry.");
|
|
1036
|
+
p.outro("");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const dl = p.spinner();
|
|
1040
|
+
dl.start(`Updating ${toUpdate.length} theme(es)...`);
|
|
1041
|
+
let successCount = 0;
|
|
1042
|
+
let failCount = 0;
|
|
1043
|
+
const dir = getThemesDir();
|
|
1044
|
+
if (!existsSync(dir)) {
|
|
1045
|
+
mkdirSync(dir, {
|
|
1046
|
+
recursive: true
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
for (const entry of toUpdate){
|
|
1050
|
+
try {
|
|
1051
|
+
const data = await fetchThemeJson(entry.name);
|
|
1052
|
+
if (!validateTheme(data)) {
|
|
1053
|
+
failCount++;
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const slug = entry.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1057
|
+
const moduleSource = generateModule(data);
|
|
1058
|
+
const target = join(dir, `${slug}.ts`);
|
|
1059
|
+
await writeFile(target, moduleSource, "utf-8");
|
|
1060
|
+
successCount++;
|
|
1061
|
+
} catch (unused) {
|
|
1062
|
+
failCount++;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
await regenerateIndex(dir);
|
|
1066
|
+
dl.stop(`Updated ${successCount} theme(es)`);
|
|
1067
|
+
if (failCount > 0) {
|
|
1068
|
+
p.log.warn(`Failed to update ${failCount} theme(es)`);
|
|
1069
|
+
}
|
|
1070
|
+
p.outro("Done!");
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const __filename$1 = __node_cjsUrl.fileURLToPath(import.meta.url);
|
|
1074
|
+
__node_cjsPath.dirname(__filename$1);
|
|
1075
|
+
|
|
1076
|
+
const COMMANDS = {
|
|
1077
|
+
add,
|
|
1078
|
+
a: add,
|
|
1079
|
+
find,
|
|
1080
|
+
search: find,
|
|
1081
|
+
f: find,
|
|
1082
|
+
s: find,
|
|
1083
|
+
list,
|
|
1084
|
+
ls: list,
|
|
1085
|
+
remove,
|
|
1086
|
+
rm: remove,
|
|
1087
|
+
check,
|
|
1088
|
+
update,
|
|
1089
|
+
upgrade: update,
|
|
1090
|
+
init,
|
|
1091
|
+
theme: async (args)=>{
|
|
1092
|
+
if (args[0] !== "init") {
|
|
1093
|
+
var _args_;
|
|
1094
|
+
p.log.error(`Unknown theme command: ${(_args_ = args[0]) != null ? _args_ : ""}`);
|
|
1095
|
+
p.log.message("Run @litlab/audx theme init to create a new theme.");
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
await themeInit(args.slice(1));
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
function showBanner() {
|
|
1102
|
+
p.intro("@litlab/audx");
|
|
1103
|
+
p.log.message("Manage sound themes for your project.");
|
|
1104
|
+
p.log.message([
|
|
1105
|
+
"Themes",
|
|
1106
|
+
" add [sound] Install an individual sound",
|
|
1107
|
+
" add Browse and install themes",
|
|
1108
|
+
" find [query] Search for themes",
|
|
1109
|
+
" list List installed themes",
|
|
1110
|
+
" remove Remove installed themes"
|
|
1111
|
+
].join("\n"));
|
|
1112
|
+
p.log.message([
|
|
1113
|
+
"Updates",
|
|
1114
|
+
" check Check for updates",
|
|
1115
|
+
" update Update installed themes"
|
|
1116
|
+
].join("\n"));
|
|
1117
|
+
p.log.message([
|
|
1118
|
+
"Project",
|
|
1119
|
+
" init Set up AudX and install themes",
|
|
1120
|
+
" theme init Create a new local theme JSON"
|
|
1121
|
+
].join("\n"));
|
|
1122
|
+
p.outro("try: npx @litlab/audx add ommgh/audio");
|
|
1123
|
+
}
|
|
1124
|
+
function showHelp() {
|
|
1125
|
+
p.intro("@litlab/audx");
|
|
1126
|
+
p.log.message([
|
|
1127
|
+
"Usage: @litlab/audx <command> [options]",
|
|
1128
|
+
"",
|
|
1129
|
+
"Manage Themes:",
|
|
1130
|
+
" add [sound] Install an individual sound",
|
|
1131
|
+
" add Browse and install themes",
|
|
1132
|
+
" add <source> Install themes from a source",
|
|
1133
|
+
" find [query] Search for themes in the registry",
|
|
1134
|
+
" list, ls List installed themes",
|
|
1135
|
+
" remove, rm Remove installed themes",
|
|
1136
|
+
"",
|
|
1137
|
+
"Updates:",
|
|
1138
|
+
" check Check for available updates",
|
|
1139
|
+
" update Update all installed themes",
|
|
1140
|
+
"",
|
|
1141
|
+
"Project:",
|
|
1142
|
+
" init Set up AudX and install themes",
|
|
1143
|
+
" theme init Create a new local theme JSON"
|
|
1144
|
+
].join("\n"));
|
|
1145
|
+
p.log.message([
|
|
1146
|
+
"Add Options:",
|
|
1147
|
+
" -l, --list Preview available themes without installing",
|
|
1148
|
+
" -y, --yes Skip confirmation prompts",
|
|
1149
|
+
" --theme <name> Install a specific theme by name",
|
|
1150
|
+
"",
|
|
1151
|
+
"Remove Options:",
|
|
1152
|
+
" -y, --yes Skip confirmation prompts"
|
|
1153
|
+
].join("\n"));
|
|
1154
|
+
p.log.message([
|
|
1155
|
+
"Source Formats:",
|
|
1156
|
+
" ./local/path Local file or directory",
|
|
1157
|
+
" owner/repo GitHub shorthand",
|
|
1158
|
+
" https://github.com/user/repo Full GitHub URL",
|
|
1159
|
+
" https://...theme.json Direct URL to a theme file",
|
|
1160
|
+
" (no argument) Browse the registry"
|
|
1161
|
+
].join("\n"));
|
|
1162
|
+
p.log.message([
|
|
1163
|
+
"Options:",
|
|
1164
|
+
" --help, -h Show this help message",
|
|
1165
|
+
" --version, -v Show version number"
|
|
1166
|
+
].join("\n"));
|
|
1167
|
+
p.note([
|
|
1168
|
+
" @litlab/audx add ommgh/audio",
|
|
1169
|
+
" @litlab/audx add ./.themes/",
|
|
1170
|
+
" @litlab/audx add ommgh/audio --list",
|
|
1171
|
+
" @litlab/audx add --theme core -y",
|
|
1172
|
+
" @litlab/audx remove core -y",
|
|
1173
|
+
" @litlab/audx find ambient",
|
|
1174
|
+
" @litlab/audx check",
|
|
1175
|
+
" @litlab/audx update"
|
|
1176
|
+
].join("\n"), "Examples");
|
|
1177
|
+
p.outro("");
|
|
1178
|
+
}
|
|
1179
|
+
async function run() {
|
|
1180
|
+
const args = process.argv.slice(2);
|
|
1181
|
+
const command = args[0];
|
|
1182
|
+
if (!command) {
|
|
1183
|
+
showBanner();
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
if (command === "--help" || command === "-h") {
|
|
1187
|
+
showHelp();
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (command === "--version" || command === "-v") {
|
|
1191
|
+
try {
|
|
1192
|
+
const { readFileSync } = await import('node:fs');
|
|
1193
|
+
const { join, dirname } = await import('node:path');
|
|
1194
|
+
const { fileURLToPath } = await import('node:url');
|
|
1195
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1196
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
1197
|
+
console.log(pkg.version);
|
|
1198
|
+
} catch (unused) {
|
|
1199
|
+
console.log("0.0.0");
|
|
1200
|
+
}
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const handler = COMMANDS[command];
|
|
1204
|
+
if (!handler) {
|
|
1205
|
+
p.log.error(`Unknown command: ${command}`);
|
|
1206
|
+
p.log.message("Run @litlab/audx --help for usage information.");
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
await handler(args.slice(1));
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
run();
|