@orxataguy/tyr 1.0.41 → 1.0.43
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 +2 -2
- package/package.json +1 -1
- package/src/core/Kernel.ts +1 -1
- package/src/core/sys/config.ts +10 -0
- package/src/core/sys/modules.ts +38 -11
package/README.md
CHANGED
|
@@ -184,7 +184,7 @@ Third parties can distribute commands as a `manifest.json` — a flat JSON map o
|
|
|
184
184
|
}
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
Register one with `tyr --add`, which validates the manifest, records it in `~/.tyr/imported_modules.
|
|
187
|
+
Register one with `tyr --add`, which validates the manifest, records it in `~/.tyr/imported_modules.yaml`, and immediately downloads whatever commands are missing:
|
|
188
188
|
|
|
189
189
|
```bash
|
|
190
190
|
tyr --add https://raw.githubusercontent.com/someuser/tyr-modules/main/manifest.json my-module
|
|
@@ -200,7 +200,7 @@ Rules worth knowing:
|
|
|
200
200
|
|
|
201
201
|
- 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.
|
|
202
202
|
- Hand-written commands are never touched, even if a manifest later defines a command with the same name.
|
|
203
|
-
- If two modules define the same command name, the one processed last (last entry in `imported_modules.
|
|
203
|
+
- If two modules define the same command name, the one processed last (last entry in `imported_modules.yaml`) wins.
|
|
204
204
|
- Only `https://` URLs are accepted, both for manifests and for the command files they point to.
|
|
205
205
|
|
|
206
206
|
Which commands came from which module is tracked in `~/.tyr/imported_modules.lock.yml`, generated automatically — you shouldn't need to edit it by hand.
|
package/package.json
CHANGED
package/src/core/Kernel.ts
CHANGED
|
@@ -110,7 +110,7 @@ export class Kernel {
|
|
|
110
110
|
console.log('Run: tyr --config --repo <url> to link it.');
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
const modulesPath = path.join(this.userRoot, 'imported_modules.
|
|
113
|
+
const modulesPath = path.join(this.userRoot, 'imported_modules.yaml');
|
|
114
114
|
if (fs.existsSync(modulesPath)) {
|
|
115
115
|
console.log('\nUpdating imported modules...');
|
|
116
116
|
const updateContext = {
|
package/src/core/sys/config.ts
CHANGED
|
@@ -284,6 +284,10 @@ export default function config({ logger, fs: tyrFs, frameworkRoot, shell }: TyrC
|
|
|
284
284
|
await tyrFs.write(mapPath, 'commands: {}\n');
|
|
285
285
|
logger.success(`File created: ${mapPath}`);
|
|
286
286
|
|
|
287
|
+
const importedModulesPath = path.join(userRoot, 'imported_modules.yaml');
|
|
288
|
+
await tyrFs.write(importedModulesPath, 'modules: {}\n');
|
|
289
|
+
logger.success(`File created: ${importedModulesPath}`);
|
|
290
|
+
|
|
287
291
|
const envPath = path.join(userRoot, '.env.example');
|
|
288
292
|
if (!tyrFs.exists(envPath)) {
|
|
289
293
|
await tyrFs.write(envPath, ENV_TEMPLATE);
|
|
@@ -331,6 +335,12 @@ export default function config({ logger, fs: tyrFs, frameworkRoot, shell }: TyrC
|
|
|
331
335
|
}
|
|
332
336
|
}
|
|
333
337
|
|
|
338
|
+
const importedModulesPath = path.join(userRoot, 'imported_modules.yaml');
|
|
339
|
+
if (!tyrFs.exists(importedModulesPath)) {
|
|
340
|
+
await tyrFs.write(importedModulesPath, 'modules: {}\n');
|
|
341
|
+
logger.success(`File created: ${importedModulesPath}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
334
344
|
const packageJsonPath = path.join(userRoot, 'package.json');
|
|
335
345
|
const tsconfigPath = path.join(userRoot, 'tsconfig.json');
|
|
336
346
|
let needsInstall = false;
|
package/src/core/sys/modules.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface SyncSummary {
|
|
|
29
29
|
failed: { command: string; reason: string }[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const IMPORTED_MODULES_FILE = 'imported_modules.
|
|
32
|
+
const IMPORTED_MODULES_FILE = 'imported_modules.yaml';
|
|
33
33
|
const LOCK_FILE = 'imported_modules.lock.yml';
|
|
34
34
|
|
|
35
35
|
function isValidHttpsUrl(value: string): boolean {
|
|
@@ -41,8 +41,19 @@ function isValidHttpsUrl(value: string): boolean {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Allows namespaced command names like 'db:migrate', but not a leading/
|
|
45
|
+
// trailing/doubled ':' (e.g. ':foo', 'foo:', 'a::b'), since those would
|
|
46
|
+
// produce an empty segment.
|
|
44
47
|
function isValidCommandName(name: string): boolean {
|
|
45
|
-
return /^[a-zA-Z0-9_-]
|
|
48
|
+
return /^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*$/.test(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ':' is a valid character in a command name (namespacing, e.g. 'db:migrate')
|
|
52
|
+
// but isn't a safe filename component on every platform (Windows reserves it).
|
|
53
|
+
// Only used when deriving the on-disk file name — the real name with its
|
|
54
|
+
// colons is still what's stored in map.yml, the lockfile, and the manifest.
|
|
55
|
+
function toSafeFilename(commandName: string): string {
|
|
56
|
+
return commandName.replace(/:/g, '-');
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
function slugFromUrl(url: string): string {
|
|
@@ -101,7 +112,7 @@ async function fetchManifest(web: any, manifestUrl: string): Promise<Record<stri
|
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
/**
|
|
104
|
-
* Reconciles ~/.tyr/imported_modules.
|
|
115
|
+
* Reconciles ~/.tyr/imported_modules.yaml against ~/.tyr/map.yml.
|
|
105
116
|
*
|
|
106
117
|
* - A command that doesn't exist yet in map.yml is installed.
|
|
107
118
|
* - A command that already exists and isn't tracked in the lockfile is left
|
|
@@ -111,7 +122,7 @@ async function fetchManifest(web: any, manifestUrl: string): Promise<Record<stri
|
|
|
111
122
|
* silently.
|
|
112
123
|
* - If two modules define the same command name, the last one processed
|
|
113
124
|
* wins (modules are processed in the order they appear in
|
|
114
|
-
* imported_modules.
|
|
125
|
+
* imported_modules.yaml).
|
|
115
126
|
*/
|
|
116
127
|
export async function syncModules(context: TyrContext, options: { force?: boolean } = {}): Promise<SyncSummary> {
|
|
117
128
|
const { logger, fs, web, userRoot } = context as any;
|
|
@@ -155,6 +166,9 @@ export async function syncModules(context: TyrContext, options: { force?: boolea
|
|
|
155
166
|
} catch (e) {
|
|
156
167
|
const reason = e instanceof Error ? e.message : String(e);
|
|
157
168
|
logger.error(`Could not read manifest for module '${moduleName}': ${reason}`);
|
|
169
|
+
// logger.error() is silenced unless --debug is passed, so mirror it via
|
|
170
|
+
// logger.info() too — otherwise this failure would be entirely invisible.
|
|
171
|
+
logger.info(`Skipped module '${moduleName}': ${reason}`);
|
|
158
172
|
summary.failed.push({ command: moduleName, reason });
|
|
159
173
|
continue;
|
|
160
174
|
}
|
|
@@ -180,14 +194,15 @@ export async function syncModules(context: TyrContext, options: { force?: boolea
|
|
|
180
194
|
const content = await web.get(fileUrl);
|
|
181
195
|
const fileContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
182
196
|
|
|
183
|
-
const
|
|
197
|
+
const fileName = toSafeFilename(commandName);
|
|
198
|
+
const destPath = path.join(userRoot, 'commands', `${fileName}.tyr.ts`);
|
|
184
199
|
await fs.write(destPath, fileContent);
|
|
185
200
|
|
|
186
201
|
if (managed && managed.module !== moduleName) {
|
|
187
202
|
logger.warn(`'${commandName}' was previously managed by module '${managed.module}'; now owned by '${moduleName}'.`);
|
|
188
203
|
}
|
|
189
204
|
|
|
190
|
-
mapConfig.commands[commandName] = `./commands/${
|
|
205
|
+
mapConfig.commands[commandName] = `./commands/${fileName}.tyr.ts`;
|
|
191
206
|
lock.commands[commandName] = { module: moduleName, manifest: manifestUrl, source: fileUrl };
|
|
192
207
|
mapChanged = true;
|
|
193
208
|
lockChanged = true;
|
|
@@ -202,6 +217,7 @@ export async function syncModules(context: TyrContext, options: { force?: boolea
|
|
|
202
217
|
} catch (e) {
|
|
203
218
|
const reason = e instanceof Error ? e.message : String(e);
|
|
204
219
|
logger.error(`Could not download '${commandName}' from '${fileUrl}': ${reason}`);
|
|
220
|
+
logger.info(`Skipped '${commandName}': ${reason}`);
|
|
205
221
|
summary.failed.push({ command: commandName, reason });
|
|
206
222
|
}
|
|
207
223
|
}
|
|
@@ -215,11 +231,18 @@ export async function syncModules(context: TyrContext, options: { force?: boolea
|
|
|
215
231
|
`skipped: ${summary.skipped.length}, failed: ${summary.failed.length}.`
|
|
216
232
|
);
|
|
217
233
|
|
|
234
|
+
if (summary.failed.length > 0) {
|
|
235
|
+
logger.info('Failures:');
|
|
236
|
+
for (const { command, reason } of summary.failed) {
|
|
237
|
+
logger.info(` - ${command}: ${reason}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
218
241
|
return summary;
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
/**
|
|
222
|
-
* Registers a manifest URL under a module name in imported_modules.
|
|
245
|
+
* Registers a manifest URL under a module name in imported_modules.yaml and
|
|
223
246
|
* immediately syncs it (downloads whatever commands are missing).
|
|
224
247
|
*/
|
|
225
248
|
export async function addModule(context: TyrContext, manifestUrl: string, name?: string): Promise<void> {
|
|
@@ -233,6 +256,7 @@ export async function addModule(context: TyrContext, manifestUrl: string, name?:
|
|
|
233
256
|
|
|
234
257
|
if (!isValidHttpsUrl(manifestUrl)) {
|
|
235
258
|
logger.error('Manifest URL must be a valid https:// URL.');
|
|
259
|
+
logger.info(`Could not add module: '${manifestUrl}' is not a valid https:// URL.`);
|
|
236
260
|
return;
|
|
237
261
|
}
|
|
238
262
|
|
|
@@ -242,6 +266,7 @@ export async function addModule(context: TyrContext, manifestUrl: string, name?:
|
|
|
242
266
|
} catch (e) {
|
|
243
267
|
const reason = e instanceof Error ? e.message : String(e);
|
|
244
268
|
logger.error(`Could not add module: ${reason}`);
|
|
269
|
+
logger.info(`Could not add module: ${reason}`);
|
|
245
270
|
return;
|
|
246
271
|
}
|
|
247
272
|
|
|
@@ -313,12 +338,14 @@ export async function generateManifest(context: TyrContext): Promise<void> {
|
|
|
313
338
|
remoteUrl = await shell.exec('git remote get-url origin');
|
|
314
339
|
} catch {
|
|
315
340
|
logger.error("~/.tyr's repository has no 'origin' remote configured. Cannot generate manifest.json.");
|
|
341
|
+
logger.info("Cannot generate manifest.json: ~/.tyr's repository has no 'origin' remote configured.");
|
|
316
342
|
return;
|
|
317
343
|
}
|
|
318
344
|
|
|
319
345
|
const repoInfo = parseGithubRemote(remoteUrl);
|
|
320
346
|
if (!repoInfo) {
|
|
321
347
|
logger.error(`Cannot generate manifest.json: only GitHub repositories are supported (raw.githubusercontent.com). Remote was: ${remoteUrl}`);
|
|
348
|
+
logger.info(`Cannot generate manifest.json: only GitHub repositories are supported. Remote was: ${remoteUrl}`);
|
|
322
349
|
return;
|
|
323
350
|
}
|
|
324
351
|
|
|
@@ -328,10 +355,10 @@ export async function generateManifest(context: TyrContext): Promise<void> {
|
|
|
328
355
|
if (currentBranch && currentBranch !== 'HEAD') {
|
|
329
356
|
branch = currentBranch;
|
|
330
357
|
} else {
|
|
331
|
-
logger.
|
|
358
|
+
logger.info(`Could not determine the current branch (detached HEAD?). Falling back to '${branch}'.`);
|
|
332
359
|
}
|
|
333
360
|
} catch {
|
|
334
|
-
logger.
|
|
361
|
+
logger.info(`Could not determine the current branch. Falling back to '${branch}'.`);
|
|
335
362
|
}
|
|
336
363
|
|
|
337
364
|
const mapPath = path.join(userRoot, 'map.yml');
|
|
@@ -347,7 +374,7 @@ export async function generateManifest(context: TyrContext): Promise<void> {
|
|
|
347
374
|
: scriptPath.replace(/^\.[/\\]/, '');
|
|
348
375
|
|
|
349
376
|
if (repoRelativePath.startsWith('..')) {
|
|
350
|
-
logger.
|
|
377
|
+
logger.info(`Skipping '${name}': its file lives outside ~/.tyr and can't be published.`);
|
|
351
378
|
skipped.push(name);
|
|
352
379
|
continue;
|
|
353
380
|
}
|
|
@@ -357,7 +384,7 @@ export async function generateManifest(context: TyrContext): Promise<void> {
|
|
|
357
384
|
}
|
|
358
385
|
|
|
359
386
|
if (Object.keys(manifest).length === 0) {
|
|
360
|
-
logger.
|
|
387
|
+
logger.info('No commands to include in manifest.json.');
|
|
361
388
|
}
|
|
362
389
|
|
|
363
390
|
const manifestPath = path.join(userRoot, 'manifest.json');
|