@quenty/nevermore-cli-helpers 1.8.1 → 1.10.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +10 -47
  2. package/dist/auth/cookie/cookie-parser.d.ts +7 -0
  3. package/dist/auth/cookie/cookie-parser.d.ts.map +1 -0
  4. package/dist/auth/cookie/cookie-parser.js +18 -0
  5. package/dist/auth/cookie/cookie-parser.js.map +1 -0
  6. package/dist/auth/cookie/cookie-parser.test.d.ts +2 -0
  7. package/dist/auth/cookie/cookie-parser.test.d.ts.map +1 -0
  8. package/dist/auth/cookie/cookie-parser.test.js +32 -0
  9. package/dist/auth/cookie/cookie-parser.test.js.map +1 -0
  10. package/dist/auth/cookie/index.d.ts +41 -0
  11. package/dist/auth/cookie/index.d.ts.map +1 -0
  12. package/dist/auth/cookie/index.js +188 -0
  13. package/dist/auth/cookie/index.js.map +1 -0
  14. package/dist/auth/cookie/linux.d.ts +14 -0
  15. package/dist/auth/cookie/linux.d.ts.map +1 -0
  16. package/dist/auth/cookie/linux.js +147 -0
  17. package/dist/auth/cookie/linux.js.map +1 -0
  18. package/dist/auth/cookie/macos.d.ts +2 -0
  19. package/dist/auth/cookie/macos.d.ts.map +1 -0
  20. package/dist/auth/cookie/macos.js +66 -0
  21. package/dist/auth/cookie/macos.js.map +1 -0
  22. package/dist/auth/cookie/validate-cookie.test.d.ts +2 -0
  23. package/dist/auth/cookie/validate-cookie.test.d.ts.map +1 -0
  24. package/dist/auth/cookie/validate-cookie.test.js +27 -0
  25. package/dist/auth/cookie/validate-cookie.test.js.map +1 -0
  26. package/dist/auth/cookie/windows.d.ts +2 -0
  27. package/dist/auth/cookie/windows.d.ts.map +1 -0
  28. package/dist/auth/cookie/windows.js +76 -0
  29. package/dist/auth/cookie/windows.js.map +1 -0
  30. package/dist/auth/open-cloud/credential-store.d.ts +14 -0
  31. package/dist/auth/open-cloud/credential-store.d.ts.map +1 -0
  32. package/dist/auth/open-cloud/credential-store.js +108 -0
  33. package/dist/auth/open-cloud/credential-store.js.map +1 -0
  34. package/dist/utils.d.ts +5 -0
  35. package/dist/utils.d.ts.map +1 -1
  36. package/dist/utils.js +3 -0
  37. package/dist/utils.js.map +1 -1
  38. package/dist/version-checker.d.ts +2 -0
  39. package/dist/version-checker.d.ts.map +1 -1
  40. package/dist/version-checker.js +22 -20
  41. package/dist/version-checker.js.map +1 -1
  42. package/package.json +8 -4
  43. package/src/auth/cookie/cookie-parser.test.ts +38 -0
  44. package/src/auth/cookie/cookie-parser.ts +18 -0
  45. package/src/auth/cookie/index.ts +271 -0
  46. package/src/auth/cookie/linux.ts +175 -0
  47. package/src/auth/cookie/macos.ts +88 -0
  48. package/src/auth/cookie/validate-cookie.test.ts +39 -0
  49. package/src/auth/cookie/windows.ts +96 -0
  50. package/src/auth/open-cloud/credential-store.ts +149 -0
  51. package/src/utils.ts +25 -0
  52. package/src/version-checker.ts +28 -24
  53. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,149 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import inquirer from 'inquirer';
5
+ import { OutputHelper } from '@quenty/cli-output-helpers';
6
+
7
+ export interface CredentialArgs {
8
+ apiKey?: string;
9
+ yes?: boolean;
10
+ }
11
+
12
+ const CREDENTIALS_DIR = path.join(os.homedir(), '.nevermore');
13
+ const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, 'credentials.json');
14
+
15
+ export function printApiKeySetupHelp(): void {
16
+ console.log('');
17
+ OutputHelper.info(
18
+ 'Create an API key at https://create.roblox.com/dashboard/credentials'
19
+ );
20
+ console.log('');
21
+ console.log('Required permissions:');
22
+ console.log(' - universe-places:write (upload place files)');
23
+ console.log(' - universe.place.luau-execution-session:write (run scripts)');
24
+ console.log(
25
+ ' - universe.place.luau-execution-session:read (read results)'
26
+ );
27
+ console.log(
28
+ ' - legacy-asset:manage (download base places for integration builds)'
29
+ );
30
+ console.log('');
31
+ }
32
+
33
+ interface StoredCredentials {
34
+ apiKey: string;
35
+ }
36
+
37
+ export async function loadStoredApiKeyAsync(): Promise<string | undefined> {
38
+ try {
39
+ const content = await fs.readFile(CREDENTIALS_PATH, 'utf-8');
40
+ const credentials = JSON.parse(content) as StoredCredentials;
41
+ return credentials.apiKey;
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ export async function saveApiKeyAsync(apiKey: string): Promise<void> {
48
+ await fs.mkdir(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
49
+ const credentials: StoredCredentials = { apiKey };
50
+ await fs.writeFile(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), {
51
+ mode: 0o600,
52
+ });
53
+ }
54
+
55
+ export async function clearApiKeyAsync(): Promise<void> {
56
+ try {
57
+ await fs.unlink(CREDENTIALS_PATH);
58
+ } catch {
59
+ // Already gone
60
+ }
61
+ }
62
+
63
+ export async function getApiKeyAsync(args: CredentialArgs): Promise<string> {
64
+ if (args.apiKey) {
65
+ return args.apiKey;
66
+ }
67
+
68
+ if (process.env.ROBLOX_OPEN_CLOUD_API_KEY) {
69
+ return process.env.ROBLOX_OPEN_CLOUD_API_KEY;
70
+ }
71
+
72
+ if (process.env.ROBLOX_UNIT_TEST_API_KEY) {
73
+ return process.env.ROBLOX_UNIT_TEST_API_KEY;
74
+ }
75
+
76
+ const stored = await loadStoredApiKeyAsync();
77
+ if (stored) {
78
+ return stored;
79
+ }
80
+
81
+ if (!args.yes) {
82
+ return await promptAndSaveApiKeyAsync();
83
+ }
84
+
85
+ throw new Error(
86
+ [
87
+ 'No API key found. Provide one of:',
88
+ ' - Run: nevermore login',
89
+ ' - Set: ROBLOX_OPEN_CLOUD_API_KEY environment variable',
90
+ ' - Pass: --api-key <key>',
91
+ ].join('\n')
92
+ );
93
+ }
94
+
95
+ export async function validateApiKeyAsync(
96
+ apiKey: string
97
+ ): Promise<{ valid: boolean; reason?: string }> {
98
+ try {
99
+ const response = await fetch(
100
+ 'https://apis.roblox.com/cloud/v2/universes/0',
101
+ {
102
+ method: 'GET',
103
+ headers: { 'X-API-Key': apiKey },
104
+ }
105
+ );
106
+
107
+ if (response.status === 401) {
108
+ return { valid: false, reason: 'Invalid API key (401 Unauthorized)' };
109
+ }
110
+
111
+ return { valid: true };
112
+ } catch (err) {
113
+ return {
114
+ valid: false,
115
+ reason: `Could not reach Roblox API: ${
116
+ err instanceof Error ? err.message : 'unknown'
117
+ }`,
118
+ };
119
+ }
120
+ }
121
+
122
+ async function promptAndSaveApiKeyAsync(): Promise<string> {
123
+ OutputHelper.warn('No API key found. Starting login...');
124
+ printApiKeySetupHelp();
125
+
126
+ const { apiKey } = await inquirer.prompt([
127
+ {
128
+ type: 'password',
129
+ name: 'apiKey',
130
+ message: 'Enter your Roblox Open Cloud API key:',
131
+ mask: '*',
132
+ validate: (input: string) =>
133
+ input.length > 0 || 'API key cannot be empty',
134
+ },
135
+ ]);
136
+
137
+ OutputHelper.info('Validating API key...');
138
+ const validation = await validateApiKeyAsync(apiKey);
139
+
140
+ if (!validation.valid) {
141
+ throw new Error(`API key validation failed: ${validation.reason}`);
142
+ }
143
+
144
+ await saveApiKeyAsync(apiKey);
145
+ OutputHelper.info('API key is valid. Saved to ~/.nevermore/credentials.json');
146
+ console.log('');
147
+
148
+ return apiKey;
149
+ }
package/src/utils.ts CHANGED
@@ -1 +1,26 @@
1
1
  export { VersionChecker } from './version-checker.js';
2
+
3
+ export {
4
+ getRobloxCookieAsync,
5
+ createPlaceInUniverseAsync,
6
+ tryRenamePlaceAsync,
7
+ validateCookieAsync,
8
+ } from './auth/cookie/index.js';
9
+ export type {
10
+ RenamePlaceResult,
11
+ CookieValidationResult,
12
+ } from './auth/cookie/index.js';
13
+ export {
14
+ COOKIE_NAME,
15
+ parseStudioCookieValue,
16
+ } from './auth/cookie/cookie-parser.js';
17
+
18
+ export {
19
+ getApiKeyAsync,
20
+ loadStoredApiKeyAsync,
21
+ saveApiKeyAsync,
22
+ clearApiKeyAsync,
23
+ validateApiKeyAsync,
24
+ printApiKeySetupHelp,
25
+ } from './auth/open-cloud/credential-store.js';
26
+ export type { CredentialArgs } from './auth/open-cloud/credential-store.js';
@@ -32,6 +32,8 @@ interface VersionCheckerOptions {
32
32
  packageJsonPath?: string;
33
33
  updateCommand?: string;
34
34
  verbose?: boolean;
35
+ /** Suppress the visual banner (box). The result is still returned so callers can embed it in structured output. */
36
+ silent?: boolean;
35
37
  }
36
38
 
37
39
  interface OurVersionData {
@@ -93,30 +95,32 @@ export class VersionChecker {
93
95
  );
94
96
  }
95
97
 
96
- if (result.isLocalDev) {
97
- const name = VersionChecker._getDisplayName(options);
98
- const text = [
99
- `${name} is running in local development mode`,
100
- '',
101
- OutputHelper.formatHint(
102
- `Run '${updateCommand}' to switch to production copy`
103
- ),
104
- '',
105
- 'This will result in less errors.',
106
- ].join('\n');
107
-
108
- OutputHelper.box(text, { centered: true });
109
- } else if (result.updateAvailable) {
110
- const name = VersionChecker._getDisplayName(options);
111
- const currentyVersionDisplayName =
112
- VersionChecker._getLocalVersionDisplayName(versionData);
113
- const text = [
114
- `${name} update available: ${currentyVersionDisplayName} → ${result.latestVersion}`,
115
- '',
116
- OutputHelper.formatHint(`Run '${updateCommand}' to update`),
117
- ].join('\n');
118
-
119
- OutputHelper.box(text, { centered: true });
98
+ if (!options.silent) {
99
+ if (result.isLocalDev) {
100
+ const name = VersionChecker._getDisplayName(options);
101
+ const text = [
102
+ `${name} is running in local development mode`,
103
+ '',
104
+ OutputHelper.formatHint(
105
+ `Run '${updateCommand}' to switch to production copy`
106
+ ),
107
+ '',
108
+ 'This will result in less errors.',
109
+ ].join('\n');
110
+
111
+ OutputHelper.box(text, { centered: true });
112
+ } else if (result.updateAvailable) {
113
+ const name = VersionChecker._getDisplayName(options);
114
+ const currentyVersionDisplayName =
115
+ VersionChecker._getLocalVersionDisplayName(versionData);
116
+ const text = [
117
+ `${name} update available: ${currentyVersionDisplayName} → ${result.latestVersion}`,
118
+ '',
119
+ OutputHelper.formatHint(`Run '${updateCommand}' to update`),
120
+ ].join('\n');
121
+
122
+ OutputHelper.box(text, { centered: true });
123
+ }
120
124
  }
121
125
 
122
126
  return result;