@orxataguy/tyr 1.0.43 → 1.0.45
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 +11 -2
- package/package.json +1 -1
- package/src/core/Kernel.ts +6 -0
- package/src/core/sys/help.ts +1 -0
- package/src/core/sys/modules.ts +218 -15
package/README.md
CHANGED
|
@@ -180,22 +180,30 @@ Third parties can distribute commands as a `manifest.json` — a flat JSON map o
|
|
|
180
180
|
```json
|
|
181
181
|
{
|
|
182
182
|
"deploy": "https://raw.githubusercontent.com/someuser/tyr-modules/main/deploy.tyr.ts",
|
|
183
|
-
"rollback": "https://raw.githubusercontent.com/someuser/tyr-modules/main/rollback.tyr.ts"
|
|
183
|
+
"rollback": "https://raw.githubusercontent.com/someuser/tyr-modules/main/rollback.tyr.ts",
|
|
184
|
+
"$env": "https://raw.githubusercontent.com/someuser/tyr-modules/main/.env.example"
|
|
184
185
|
}
|
|
185
186
|
```
|
|
186
187
|
|
|
187
|
-
|
|
188
|
+
`$env` is optional and reserved (`$` can never appear in a command name, so it never collides with one). If present, it's downloaded as `~/.tyr/.env.<module-name>.example` — a template of whatever environment variables that module's commands expect. It follows the exact same install/skip/force-update rules as commands below, and `tyr --del <module-name>` removes it along with everything else that module owns.
|
|
189
|
+
|
|
190
|
+
Register one with `tyr --add`, which validates the manifest, records it in `~/.tyr/imported_modules.yaml`, and immediately downloads whatever commands (and `$env`, if present) are missing:
|
|
188
191
|
|
|
189
192
|
```bash
|
|
190
193
|
tyr --add https://raw.githubusercontent.com/someuser/tyr-modules/main/manifest.json my-module
|
|
191
194
|
```
|
|
192
195
|
|
|
196
|
+
The module name is optional. If you omit it and the manifest URL is a `raw.githubusercontent.com` link, the repository name is used (`tyr-modules` for the URL above). For any other host, it falls back to a slug of the manifest file's own name.
|
|
197
|
+
|
|
193
198
|
```bash
|
|
194
199
|
tyr --modules # list registered modules
|
|
195
200
|
tyr --modules sync # re-check all registered manifests and install anything missing
|
|
196
201
|
tyr --update # git pull ~/.tyr (if linked) and force-refresh all imported commands
|
|
202
|
+
tyr --del my-module # unregister a module: removes its commands, map.yml entries and lock records
|
|
197
203
|
```
|
|
198
204
|
|
|
205
|
+
`tyr --del <module-name>` is the counterpart to `--add` — it looks up every command the lockfile says that module owns, deletes the command file, drops it (and any alias pointing to it) from `map.yml`, clears its lockfile entry, and finally removes the module from `imported_modules.yaml`. Commands that aren't tracked as belonging to that module (hand-written, or now owned by a different module after a collision) are left untouched.
|
|
206
|
+
|
|
199
207
|
Rules worth knowing:
|
|
200
208
|
|
|
201
209
|
- A command name that already exists locally is never overwritten by a plain sync — only `tyr --update` (or `tyr --modules sync --force`) refreshes commands that were themselves installed by an import.
|
|
@@ -219,6 +227,7 @@ Commit and push it, and anyone can install your commands with `tyr --add <raw-ur
|
|
|
219
227
|
- Requires a GitHub remote named `origin` — other hosts aren't supported, since the URLs only resolve through `raw.githubusercontent.com`.
|
|
220
228
|
- If `~/.tyr` isn't a git repository (or has no `origin` remote), `--manifest` refuses and tells you why instead of generating a broken file.
|
|
221
229
|
- Commands whose file lives outside `~/.tyr` (an absolute path elsewhere on disk) are skipped, since they can't be resolved to a URL in the repo.
|
|
230
|
+
- If `~/.tyr/.env.example` exists, it's automatically included as `$env`, so whoever runs `tyr --add` on your manifest gets it as `.env.<module-name>.example`.
|
|
222
231
|
|
|
223
232
|
### `tyr chat [directory] [--port <n>] [--split <0-1>]`
|
|
224
233
|
|
package/package.json
CHANGED
package/src/core/Kernel.ts
CHANGED
|
@@ -85,6 +85,7 @@ export class Kernel {
|
|
|
85
85
|
console.log(' tyr --upgrade Upgrade Tyr to the latest npm version');
|
|
86
86
|
console.log(' tyr --modules List or sync imported modules');
|
|
87
87
|
console.log(' tyr --add <url> Register and sync a manifest.json as an imported module');
|
|
88
|
+
console.log(' tyr --del <name> Unregister an imported module and remove its commands');
|
|
88
89
|
console.log(' tyr --manifest Generate ~/.tyr/manifest.json from your commands (requires a GitHub-linked repo)');
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
@@ -194,6 +195,11 @@ export class Kernel {
|
|
|
194
195
|
return;
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
if (commandName === '--del') {
|
|
199
|
+
await modulesCommand(context)(['del', ...args.slice(1)]);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
197
203
|
if (commandName === '--manifest') {
|
|
198
204
|
await modulesCommand(context)(['manifest']);
|
|
199
205
|
return;
|
package/src/core/sys/help.ts
CHANGED
|
@@ -76,6 +76,7 @@ export default function help({ userRoot }: TyrContext) {
|
|
|
76
76
|
{ name: '--upgrade', description: 'Upgrades the tyr npm package.', usage: 'tyr --upgrade' },
|
|
77
77
|
{ name: '--modules', description: 'Lists or syncs modules imported via manifest.json.', usage: 'tyr --modules [sync|list]' },
|
|
78
78
|
{ name: '--add', description: 'Registers a manifest.json URL as an imported module and syncs it.', usage: 'tyr --add <manifest-url> [name]' },
|
|
79
|
+
{ name: '--del', description: 'Unregisters an imported module and removes the commands it owns.', usage: 'tyr --del <module-name>' },
|
|
79
80
|
{ name: '--manifest', description: 'Generates ~/.tyr/manifest.json from your commands (needs a GitHub-linked repo).', usage: 'tyr --manifest' },
|
|
80
81
|
{ name: 'gen', description: 'Generates a new command from a description using AI.', usage: 'tyr gen <name> "<description>"' },
|
|
81
82
|
{ name: 'doc', description: 'Opens the framework documentation in the browser.', usage: 'tyr doc' },
|
package/src/core/sys/modules.ts
CHANGED
|
@@ -18,8 +18,15 @@ interface ManagedCommand {
|
|
|
18
18
|
source: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
interface ManagedEnvFile {
|
|
22
|
+
manifest: string;
|
|
23
|
+
source: string;
|
|
24
|
+
file: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
interface ModulesLock {
|
|
22
28
|
commands: Record<string, ManagedCommand>;
|
|
29
|
+
env?: Record<string, ManagedEnvFile>;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export interface SyncSummary {
|
|
@@ -56,13 +63,31 @@ function toSafeFilename(commandName: string): string {
|
|
|
56
63
|
return commandName.replace(/:/g, '-');
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
function
|
|
66
|
+
function sanitizeSlug(value: string): string {
|
|
67
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
68
|
+
return slug || 'module';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Default module name when `tyr --add <url>` is called without one.
|
|
73
|
+
* For a raw.githubusercontent.com URL (the expected shape:
|
|
74
|
+
* raw.githubusercontent.com/<owner>/<repo>/<branch>/<path>) this uses the
|
|
75
|
+
* repository name, since that's what identifies where the module comes from.
|
|
76
|
+
* For any other host — a manifest can be served from anywhere, as long as
|
|
77
|
+
* it's https — falls back to a slug of the manifest file's own name.
|
|
78
|
+
*/
|
|
79
|
+
function defaultModuleNameFromUrl(url: string): string {
|
|
60
80
|
try {
|
|
61
81
|
const parsed = new URL(url);
|
|
62
|
-
const
|
|
82
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
83
|
+
|
|
84
|
+
if (parsed.hostname === 'raw.githubusercontent.com' && segments.length >= 2) {
|
|
85
|
+
return sanitizeSlug(segments[1]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const base = segments.pop() ?? 'module';
|
|
63
89
|
const withoutExt = base.replace(/\.json$/i, '').replace(/\.manifest$/i, '');
|
|
64
|
-
|
|
65
|
-
return slug || 'module';
|
|
90
|
+
return sanitizeSlug(withoutExt);
|
|
66
91
|
} catch {
|
|
67
92
|
return 'module';
|
|
68
93
|
}
|
|
@@ -82,12 +107,24 @@ async function saveYaml(fs: any, filePath: string, data: unknown): Promise<void>
|
|
|
82
107
|
await fs.write(filePath, yaml.dump(data, { indent: 2, lineWidth: -1 }));
|
|
83
108
|
}
|
|
84
109
|
|
|
110
|
+
// Reserved manifest key for an optional .env.example reference. '$' can never
|
|
111
|
+
// appear in a valid command name (see isValidCommandName), so it can't collide
|
|
112
|
+
// with a real command.
|
|
113
|
+
const ENV_MANIFEST_KEY = '$env';
|
|
114
|
+
|
|
115
|
+
interface ParsedManifest {
|
|
116
|
+
commands: Record<string, string>;
|
|
117
|
+
envUrl?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
/**
|
|
86
121
|
* Fetches a manifest.json (name -> raw file URL) and validates its shape.
|
|
87
122
|
* Only https URLs and safe command names are accepted, to avoid path
|
|
88
123
|
* traversal and to keep imported code coming from a known-good transport.
|
|
124
|
+
* A manifest may also carry a reserved `$env` key pointing at a raw
|
|
125
|
+
* .env.example file for the module's required environment variables.
|
|
89
126
|
*/
|
|
90
|
-
async function fetchManifest(web: any, manifestUrl: string): Promise<
|
|
127
|
+
async function fetchManifest(web: any, manifestUrl: string): Promise<ParsedManifest> {
|
|
91
128
|
if (!isValidHttpsUrl(manifestUrl)) {
|
|
92
129
|
throw new TyrError(`Manifest URL must use https: ${manifestUrl}`);
|
|
93
130
|
}
|
|
@@ -99,16 +136,28 @@ async function fetchManifest(web: any, manifestUrl: string): Promise<Record<stri
|
|
|
99
136
|
throw new TyrError(`Manifest at ${manifestUrl} is not a valid JSON object.`);
|
|
100
137
|
}
|
|
101
138
|
|
|
102
|
-
|
|
139
|
+
const commands: Record<string, string> = {};
|
|
140
|
+
let envUrl: string | undefined;
|
|
141
|
+
|
|
142
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
143
|
+
if (name === ENV_MANIFEST_KEY) {
|
|
144
|
+
if (typeof value !== 'string' || !isValidHttpsUrl(value)) {
|
|
145
|
+
throw new TyrError(`Manifest at ${manifestUrl} has an invalid (non-https) URL for '${ENV_MANIFEST_KEY}'.`);
|
|
146
|
+
}
|
|
147
|
+
envUrl = value;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
103
151
|
if (!isValidCommandName(name)) {
|
|
104
152
|
throw new TyrError(`Manifest at ${manifestUrl} has an invalid command name: '${name}'`);
|
|
105
153
|
}
|
|
106
|
-
if (typeof
|
|
154
|
+
if (typeof value !== 'string' || !isValidHttpsUrl(value)) {
|
|
107
155
|
throw new TyrError(`Manifest at ${manifestUrl} has an invalid (non-https) URL for command '${name}'.`);
|
|
108
156
|
}
|
|
157
|
+
commands[name] = value;
|
|
109
158
|
}
|
|
110
159
|
|
|
111
|
-
return
|
|
160
|
+
return { commands, envUrl };
|
|
112
161
|
}
|
|
113
162
|
|
|
114
163
|
/**
|
|
@@ -145,6 +194,7 @@ export async function syncModules(context: TyrContext, options: { force?: boolea
|
|
|
145
194
|
|
|
146
195
|
if (!mapConfig.commands) mapConfig.commands = {};
|
|
147
196
|
if (!lock.commands) lock.commands = {};
|
|
197
|
+
if (!lock.env) lock.env = {};
|
|
148
198
|
|
|
149
199
|
// Snapshot the state as it was *before* this run. Collisions between two
|
|
150
200
|
// modules processed in the same run are resolved by processing order
|
|
@@ -160,9 +210,9 @@ export async function syncModules(context: TyrContext, options: { force?: boolea
|
|
|
160
210
|
for (const [moduleName, manifestUrl] of Object.entries(modulesFile.modules ?? {})) {
|
|
161
211
|
logger.info(`Syncing module '${moduleName}' (${manifestUrl})...`);
|
|
162
212
|
|
|
163
|
-
let
|
|
213
|
+
let parsedManifest: ParsedManifest;
|
|
164
214
|
try {
|
|
165
|
-
|
|
215
|
+
parsedManifest = await fetchManifest(web, manifestUrl);
|
|
166
216
|
} catch (e) {
|
|
167
217
|
const reason = e instanceof Error ? e.message : String(e);
|
|
168
218
|
logger.error(`Could not read manifest for module '${moduleName}': ${reason}`);
|
|
@@ -173,6 +223,8 @@ export async function syncModules(context: TyrContext, options: { force?: boolea
|
|
|
173
223
|
continue;
|
|
174
224
|
}
|
|
175
225
|
|
|
226
|
+
const { commands: manifest, envUrl } = parsedManifest;
|
|
227
|
+
|
|
176
228
|
for (const [commandName, fileUrl] of Object.entries(manifest)) {
|
|
177
229
|
const alreadyInMap = !!mapSnapshot[commandName];
|
|
178
230
|
const managed = lockSnapshot[commandName];
|
|
@@ -221,6 +273,39 @@ export async function syncModules(context: TyrContext, options: { force?: boolea
|
|
|
221
273
|
summary.failed.push({ command: commandName, reason });
|
|
222
274
|
}
|
|
223
275
|
}
|
|
276
|
+
|
|
277
|
+
if (envUrl) {
|
|
278
|
+
const envFileName = `.env.${toSafeFilename(moduleName)}.example`;
|
|
279
|
+
const envDestPath = path.join(userRoot, envFileName);
|
|
280
|
+
const envManaged = lock.env![moduleName];
|
|
281
|
+
|
|
282
|
+
const shouldInstallEnv = !fs.exists(envDestPath);
|
|
283
|
+
const shouldForceUpdateEnv = force && !!envManaged;
|
|
284
|
+
|
|
285
|
+
if (!shouldInstallEnv && !shouldForceUpdateEnv) {
|
|
286
|
+
logger.info(
|
|
287
|
+
envManaged
|
|
288
|
+
? `Skipping ${envFileName}: already installed. Use 'tyr --update' to refresh.`
|
|
289
|
+
: `Skipping ${envFileName}: a file with that name already exists and isn't managed by an import.`
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
try {
|
|
293
|
+
const envContent = await web.get(envUrl);
|
|
294
|
+
const envFileContent = typeof envContent === 'string' ? envContent : JSON.stringify(envContent, null, 2);
|
|
295
|
+
await fs.write(envDestPath, envFileContent);
|
|
296
|
+
|
|
297
|
+
lock.env![moduleName] = { manifest: manifestUrl, source: envUrl, file: envFileName };
|
|
298
|
+
lockChanged = true;
|
|
299
|
+
|
|
300
|
+
logger.success(`${shouldInstallEnv ? 'Installed' : 'Updated'} ${envFileName} from module '${moduleName}'.`);
|
|
301
|
+
} catch (e) {
|
|
302
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
303
|
+
logger.error(`Could not download ${envFileName} for module '${moduleName}': ${reason}`);
|
|
304
|
+
logger.info(`Could not download ${envFileName} for module '${moduleName}': ${reason}`);
|
|
305
|
+
summary.failed.push({ command: envFileName, reason });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
224
309
|
}
|
|
225
310
|
|
|
226
311
|
if (mapChanged) await saveYaml(fs, mapPath, mapConfig);
|
|
@@ -270,7 +355,7 @@ export async function addModule(context: TyrContext, manifestUrl: string, name?:
|
|
|
270
355
|
return;
|
|
271
356
|
}
|
|
272
357
|
|
|
273
|
-
const moduleName = name && isValidCommandName(name) ? name :
|
|
358
|
+
const moduleName = name && isValidCommandName(name) ? name : defaultModuleNameFromUrl(manifestUrl);
|
|
274
359
|
|
|
275
360
|
const modulesPath = path.join(userRoot, IMPORTED_MODULES_FILE);
|
|
276
361
|
const modulesFile = await loadYaml<ImportedModulesFile>(fs, modulesPath, { modules: {} });
|
|
@@ -287,6 +372,104 @@ export async function addModule(context: TyrContext, manifestUrl: string, name?:
|
|
|
287
372
|
await syncModules(context);
|
|
288
373
|
}
|
|
289
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Unregisters a module: removes it from imported_modules.yaml, deletes every
|
|
377
|
+
* command file it's tracked as owning in the lockfile, drops those commands
|
|
378
|
+
* (and any aliases pointing at them) from map.yml, deletes its
|
|
379
|
+
* `.env.<module>.example` if one was imported, and clears their lockfile
|
|
380
|
+
* entries. The counterpart to `tyr --add` — closes the loop.
|
|
381
|
+
*/
|
|
382
|
+
export async function removeModule(context: TyrContext, moduleName: string): Promise<void> {
|
|
383
|
+
const { logger, fs, userRoot } = context as any;
|
|
384
|
+
|
|
385
|
+
if (!moduleName) {
|
|
386
|
+
logger.error('Missing module name.');
|
|
387
|
+
logger.info('Usage: tyr --del <module-name>');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const modulesPath = path.join(userRoot, IMPORTED_MODULES_FILE);
|
|
392
|
+
const mapPath = path.join(userRoot, 'map.yml');
|
|
393
|
+
const lockPath = path.join(userRoot, LOCK_FILE);
|
|
394
|
+
|
|
395
|
+
const modulesFile = await loadYaml<ImportedModulesFile>(fs, modulesPath, { modules: {} });
|
|
396
|
+
const mapConfig = await loadYaml<TyrConfig>(fs, mapPath, { commands: {} });
|
|
397
|
+
const lock = await loadYaml<ModulesLock>(fs, lockPath, { commands: {} });
|
|
398
|
+
|
|
399
|
+
if (!modulesFile.modules) modulesFile.modules = {};
|
|
400
|
+
if (!mapConfig.commands) mapConfig.commands = {};
|
|
401
|
+
if (!lock.commands) lock.commands = {};
|
|
402
|
+
if (!lock.env) lock.env = {};
|
|
403
|
+
|
|
404
|
+
const isRegistered = !!modulesFile.modules[moduleName];
|
|
405
|
+
const managedCommands = Object.entries(lock.commands)
|
|
406
|
+
.filter(([, info]) => info.module === moduleName)
|
|
407
|
+
.map(([commandName]) => commandName);
|
|
408
|
+
const managedEnv = lock.env[moduleName];
|
|
409
|
+
|
|
410
|
+
if (!isRegistered && managedCommands.length === 0 && !managedEnv) {
|
|
411
|
+
logger.error(`Module '${moduleName}' is not registered and owns no commands. Nothing to remove.`);
|
|
412
|
+
logger.info(`Module '${moduleName}' was not found in ${IMPORTED_MODULES_FILE} or ${LOCK_FILE}.`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let mapChanged = false;
|
|
417
|
+
let lockChanged = false;
|
|
418
|
+
|
|
419
|
+
for (const commandName of managedCommands) {
|
|
420
|
+
const scriptPath = mapConfig.commands[commandName];
|
|
421
|
+
if (scriptPath) {
|
|
422
|
+
const absolutePath = path.isAbsolute(scriptPath) ? scriptPath : path.resolve(userRoot, scriptPath);
|
|
423
|
+
if (fs.exists(absolutePath)) {
|
|
424
|
+
await fs.delete(absolutePath);
|
|
425
|
+
}
|
|
426
|
+
delete mapConfig.commands[commandName];
|
|
427
|
+
mapChanged = true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (mapConfig.aliases) {
|
|
431
|
+
for (const [alias, target] of Object.entries(mapConfig.aliases)) {
|
|
432
|
+
if (target === commandName) {
|
|
433
|
+
delete mapConfig.aliases[alias];
|
|
434
|
+
mapChanged = true;
|
|
435
|
+
logger.info(`Alias '${alias}' removed (pointed to '${commandName}').`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
delete lock.commands[commandName];
|
|
441
|
+
lockChanged = true;
|
|
442
|
+
|
|
443
|
+
logger.success(`Removed command '${commandName}' (was managed by module '${moduleName}').`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (managedEnv) {
|
|
447
|
+
const envPath = path.join(userRoot, managedEnv.file);
|
|
448
|
+
if (fs.exists(envPath)) {
|
|
449
|
+
await fs.delete(envPath);
|
|
450
|
+
}
|
|
451
|
+
delete lock.env[moduleName];
|
|
452
|
+
lockChanged = true;
|
|
453
|
+
logger.success(`Removed ${managedEnv.file} (was managed by module '${moduleName}').`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (isRegistered) {
|
|
457
|
+
delete modulesFile.modules[moduleName];
|
|
458
|
+
await saveYaml(fs, modulesPath, modulesFile);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (mapChanged) await saveYaml(fs, mapPath, mapConfig);
|
|
462
|
+
if (lockChanged) await saveYaml(fs, lockPath, lock);
|
|
463
|
+
|
|
464
|
+
const removedItems = [...managedCommands, ...(managedEnv ? [managedEnv.file] : [])];
|
|
465
|
+
const removedList = removedItems.length ? `: ${removedItems.join(', ')}` : '';
|
|
466
|
+
logger.success(
|
|
467
|
+
`Module '${moduleName}' removed` +
|
|
468
|
+
(removedItems.length ? ` — ${removedItems.length} item(s) removed${removedList}.` : ' (it had nothing managed).')
|
|
469
|
+
);
|
|
470
|
+
logger.info(`Module '${moduleName}' unlinked from ${IMPORTED_MODULES_FILE}${removedList}.`);
|
|
471
|
+
}
|
|
472
|
+
|
|
290
473
|
interface GithubRepoInfo {
|
|
291
474
|
owner: string;
|
|
292
475
|
repo: string;
|
|
@@ -318,6 +501,10 @@ function parseGithubRemote(remoteUrl: string): GithubRepoInfo | null {
|
|
|
318
501
|
* the publishing counterpart of `tyr --add`: whatever this produces can be
|
|
319
502
|
* handed to someone else, who registers it with `tyr --add <url>`.
|
|
320
503
|
*
|
|
504
|
+
* If ~/.tyr/.env.example exists, its raw URL is also included under the
|
|
505
|
+
* reserved `$env` key, so `tyr --add` downloads it as `.env.<module>.example`
|
|
506
|
+
* for whoever installs this manifest.
|
|
507
|
+
*
|
|
321
508
|
* Requires ~/.tyr to be a git repository linked to a GitHub remote — there's
|
|
322
509
|
* no way to build a raw.githubusercontent.com URL otherwise.
|
|
323
510
|
*/
|
|
@@ -383,16 +570,28 @@ export async function generateManifest(context: TyrContext): Promise<void> {
|
|
|
383
570
|
manifest[name] = `https://raw.githubusercontent.com/${repoInfo.owner}/${repoInfo.repo}/${branch}/${normalized}`;
|
|
384
571
|
}
|
|
385
572
|
|
|
386
|
-
|
|
573
|
+
const commandCount = Object.keys(manifest).length;
|
|
574
|
+
if (commandCount === 0) {
|
|
387
575
|
logger.info('No commands to include in manifest.json.');
|
|
388
576
|
}
|
|
389
577
|
|
|
578
|
+
// If ~/.tyr has a .env.example, publish it too — tyr --add downloads it as
|
|
579
|
+
// .env.<module>.example so whoever installs this manifest knows which
|
|
580
|
+
// environment variables these commands expect.
|
|
581
|
+
const envExamplePath = path.join(userRoot, '.env.example');
|
|
582
|
+
let includedEnv = false;
|
|
583
|
+
if (fs.exists(envExamplePath)) {
|
|
584
|
+
manifest[ENV_MANIFEST_KEY] = `https://raw.githubusercontent.com/${repoInfo.owner}/${repoInfo.repo}/${branch}/.env.example`;
|
|
585
|
+
includedEnv = true;
|
|
586
|
+
}
|
|
587
|
+
|
|
390
588
|
const manifestPath = path.join(userRoot, 'manifest.json');
|
|
391
589
|
await fs.write(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
392
590
|
|
|
393
591
|
logger.success(
|
|
394
|
-
`manifest.json generated at ${manifestPath} (${
|
|
395
|
-
`${skipped.length ? `, ${skipped.length} skipped` : ''}
|
|
592
|
+
`manifest.json generated at ${manifestPath} (${commandCount} commands` +
|
|
593
|
+
`${skipped.length ? `, ${skipped.length} skipped` : ''}` +
|
|
594
|
+
`${includedEnv ? ', includes .env.example' : ''}).`
|
|
396
595
|
);
|
|
397
596
|
logger.info("Commit and push it so others can install it with: tyr --add <raw-url-to-manifest.json>");
|
|
398
597
|
}
|
|
@@ -411,6 +610,10 @@ export default function modules(context: TyrContext) {
|
|
|
411
610
|
await addModule(context, args[1], args[2]);
|
|
412
611
|
return;
|
|
413
612
|
}
|
|
613
|
+
case 'del': {
|
|
614
|
+
await removeModule(context, args[1]);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
414
617
|
case 'manifest': {
|
|
415
618
|
await generateManifest(context);
|
|
416
619
|
return;
|
|
@@ -435,7 +638,7 @@ export default function modules(context: TyrContext) {
|
|
|
435
638
|
}
|
|
436
639
|
default:
|
|
437
640
|
logger.error(`Unknown subcommand '${sub}'.`);
|
|
438
|
-
logger.info('Usage: tyr --modules [sync|list|manifest]');
|
|
641
|
+
logger.info('Usage: tyr --modules [sync|list|manifest|del <module-name>]');
|
|
439
642
|
}
|
|
440
643
|
};
|
|
441
644
|
}
|