@juuno-sdk/cli 1.0.12 → 2.0.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 (38) hide show
  1. package/README.md +5 -3
  2. package/bin/cli-router.js +54 -72
  3. package/bin/cli.js +2 -3
  4. package/dist/assets/main-GRbDNSIa.css +1 -0
  5. package/dist/assets/main-TDlmPBlt.js +1 -0
  6. package/dist/assets/main-rFvAfxK4.js +1 -0
  7. package/dist/cli/src/auth/index.js +47 -34
  8. package/dist/cli/src/deploy/index.js +209 -115
  9. package/dist/cli/src/dev/index.js +46 -40
  10. package/dist/cli/src/info/index.js +12 -36
  11. package/dist/cli/src/list/index.js +12 -36
  12. package/dist/cli/src/login/index.js +37 -42
  13. package/dist/cli/src/logout/index.js +34 -33
  14. package/dist/cli/src/simulator/index.js +54 -39
  15. package/dist/cli/src/utils/api.js +45 -0
  16. package/dist/cli/src/utils/error.js +9 -0
  17. package/dist/cli/src/utils/is-record.js +11 -0
  18. package/dist/cli/src/whoami/index.js +37 -33
  19. package/dist/cli/tsconfig.build.tsbuildinfo +1 -1
  20. package/dist/index.html +5 -2
  21. package/dist/types/src/auth/index.d.ts +17 -0
  22. package/dist/types/src/auth/index.d.ts.map +1 -1
  23. package/dist/types/src/deploy/index.d.ts +12 -0
  24. package/dist/types/src/deploy/index.d.ts.map +1 -1
  25. package/dist/types/src/dev/index.d.ts.map +1 -1
  26. package/dist/types/src/info/index.d.ts.map +1 -1
  27. package/dist/types/src/list/index.d.ts.map +1 -1
  28. package/dist/types/src/login/index.d.ts.map +1 -1
  29. package/dist/types/src/logout/index.d.ts.map +1 -1
  30. package/dist/types/src/simulator/index.d.ts.map +1 -1
  31. package/dist/types/src/utils/api.d.ts +27 -0
  32. package/dist/types/src/utils/api.d.ts.map +1 -0
  33. package/dist/types/src/utils/error.d.ts +8 -0
  34. package/dist/types/src/utils/error.d.ts.map +1 -0
  35. package/dist/types/src/utils/is-record.d.ts +10 -0
  36. package/dist/types/src/utils/is-record.d.ts.map +1 -0
  37. package/dist/types/src/whoami/index.d.ts.map +1 -1
  38. package/package.json +28 -26
@@ -1,5 +1,31 @@
1
1
  import { loginWithCredentials, saveToken, getApiUrl, resolveProfile, formatProfileFlag, } from '../auth/index.js';
2
2
  import inquirer from 'inquirer';
3
+ import { getErrorMessage } from '../utils/error.js';
4
+ async function gatherCredentials(options) {
5
+ if (options.email && options.password) {
6
+ return { email: options.email, password: options.password };
7
+ }
8
+ const answers = await inquirer.prompt([
9
+ {
10
+ type: 'input',
11
+ name: 'email',
12
+ message: 'Email:',
13
+ when: !options.email,
14
+ validate: (input) => input.length > 0 || 'Email is required',
15
+ },
16
+ {
17
+ type: 'password',
18
+ name: 'password',
19
+ message: 'Password:',
20
+ when: !options.password,
21
+ mask: '*',
22
+ },
23
+ ]);
24
+ return {
25
+ email: options.email || answers.email,
26
+ password: options.password || answers.password,
27
+ };
28
+ }
3
29
  /**
4
30
  * Logs in to Juuno and saves authentication token for the target profile.
5
31
  */
@@ -11,54 +37,23 @@ export async function login(options) {
11
37
  console.log(` Profile: ${profile}`);
12
38
  console.log(` API: ${apiUrl}`);
13
39
  console.log('');
14
- let email = options.email;
15
- let pwd = options.password;
16
- if (!email || !pwd) {
17
- const answers = await inquirer.prompt([
18
- {
19
- type: 'input',
20
- name: 'email',
21
- message: 'Email:',
22
- when: !options.email,
23
- validate: (input) => input.length > 0 || 'Email is required',
24
- },
25
- {
26
- type: 'password',
27
- name: 'password',
28
- message: 'Password:',
29
- when: !options.password,
30
- mask: '*',
31
- },
32
- ]);
33
- email = email || answers.email;
34
- pwd = pwd || answers.password;
35
- }
36
- const credentials = {
37
- email: email,
38
- password: pwd,
39
- };
40
- console.log('');
41
- console.log('🔑 Authenticating...');
42
- let token;
43
- try {
44
- token = await loginWithCredentials(credentials, apiUrl);
45
- }
46
- catch (error) {
47
- console.error(`❌ ${error instanceof Error ? error.message : String(error)}`);
48
- process.exit(1);
49
- }
50
40
  try {
41
+ const credentials = await gatherCredentials(options);
42
+ console.log('');
43
+ console.log('🔑 Authenticating...');
44
+ const token = await loginWithCredentials(credentials, apiUrl);
51
45
  saveToken(token, profile, apiUrl);
52
46
  console.log(` ✓ Token saved to ~/.juuno/credentials.json (profile: ${profile})`);
53
47
  console.log('');
48
+ const profileFlag = formatProfileFlag(profile);
49
+ const flagSuffix = profileFlag ? ` with${profileFlag}` : '';
50
+ console.log('✅ Login successful!');
51
+ console.log('');
52
+ console.log(`💡 You can now use deploy, list, and info commands${flagSuffix} without re-authenticating.`);
53
+ console.log('');
54
54
  }
55
55
  catch (error) {
56
- console.error(`❌ Failed to save token: ${error instanceof Error ? error.message : String(error)}`);
56
+ console.error(`❌ ${getErrorMessage(error)}`);
57
57
  process.exit(1);
58
58
  }
59
- console.log('✅ Login successful!');
60
- console.log('');
61
- const profileFlag = formatProfileFlag(profile);
62
- console.log(`💡 You can now use deploy, list, and info commands${profileFlag ? ` with${profileFlag}` : ''} without re-authenticating.`);
63
- console.log('');
64
59
  }
@@ -1,47 +1,48 @@
1
1
  import { removeProfile, removeAllProfiles, listProfiles, resolveProfile, isLoggedIn, formatProfileFlag, } from '../auth/index.js';
2
- /**
3
- * Logs out of Juuno by removing stored credentials for the target profile.
4
- */
5
- export async function logout(options = {}) {
6
- console.log('📦 Juuno CLI - Logout');
7
- console.log('');
8
- if (options.all) {
9
- const profiles = listProfiles();
10
- if (profiles.length === 0) {
11
- console.log(' No profiles are currently logged in.');
12
- console.log('');
13
- return;
14
- }
15
- try {
16
- removeAllProfiles();
17
- console.log(` ✓ Removed ${profiles.length} profile${profiles.length === 1 ? '' : 's'}: ${profiles.join(', ')}`);
18
- console.log('');
19
- console.log('✅ All profiles logged out.');
20
- console.log('');
21
- }
22
- catch (error) {
23
- console.error(`❌ Failed to remove credentials: ${error instanceof Error ? error.message : String(error)}`);
24
- process.exit(1);
25
- }
2
+ import { getErrorMessage } from '../utils/error.js';
3
+ function logoutAll() {
4
+ const profiles = listProfiles();
5
+ if (profiles.length === 0) {
6
+ console.log(' No profiles are currently logged in.');
7
+ console.log('');
26
8
  return;
27
9
  }
28
- const profile = resolveProfile(options);
10
+ removeAllProfiles();
11
+ const plural = profiles.length === 1 ? '' : 's';
12
+ console.log(` ✓ Removed ${profiles.length} profile${plural}: ${profiles.join(', ')}`);
13
+ console.log('');
14
+ console.log('✅ All profiles logged out.');
15
+ console.log('');
16
+ }
17
+ function logoutOne(profile) {
29
18
  if (!isLoggedIn(profile)) {
30
19
  console.log(` Profile "${profile}" is not currently logged in.`);
31
20
  console.log('');
32
21
  return;
33
22
  }
23
+ removeProfile(profile);
24
+ console.log(` ✓ Credentials removed for profile "${profile}"`);
25
+ console.log('');
26
+ console.log('✅ Logged out successfully!');
27
+ console.log('');
28
+ console.log(`💡 Run "juuno-cli login${formatProfileFlag(profile)}" to authenticate again.`);
29
+ console.log('');
30
+ }
31
+ /**
32
+ * Logs out of Juuno by removing stored credentials for the target profile.
33
+ */
34
+ export async function logout(options = {}) {
35
+ console.log('📦 Juuno CLI - Logout');
36
+ console.log('');
34
37
  try {
35
- removeProfile(profile);
36
- console.log(` ✓ Credentials removed for profile "${profile}"`);
37
- console.log('');
38
- console.log('✅ Logged out successfully!');
39
- console.log('');
40
- console.log(`💡 Run "juuno-cli login${formatProfileFlag(profile)}" to authenticate again.`);
41
- console.log('');
38
+ if (options.all) {
39
+ logoutAll();
40
+ return;
41
+ }
42
+ logoutOne(resolveProfile(options));
42
43
  }
43
44
  catch (error) {
44
- console.error(`❌ Failed to remove credentials: ${error instanceof Error ? error.message : String(error)}`);
45
+ console.error(`❌ Failed to remove credentials: ${getErrorMessage(error)}`);
45
46
  process.exit(1);
46
47
  }
47
48
  }
@@ -45,23 +45,40 @@ async function fetchManifest(appUrl) {
45
45
  }
46
46
  }
47
47
  /**
48
- * Launch the app simulator with full options object.
48
+ * Static CDN map for the import-map slots in index.html. Adding a new
49
+ * dependency is one entry; the runtime substitution below iterates this
50
+ * record so the body never grows.
49
51
  */
50
- export async function launchSimulatorWithOptions(options) {
51
- const { appUrl, port, distPath } = options;
52
- // Fetch manifest to get actual resource paths.
53
- const manifest = await fetchManifest(appUrl);
54
- const playerJs = manifest?.resources?.player?.find((p) => p.endsWith('.js'));
55
- const configJs = manifest?.resources?.config?.find((p) => p.endsWith('.js'));
56
- const playerPath = playerJs || '/player.js';
57
- const configPath = configJs || '/config.js';
58
- // Validate that dist directory exists before attempting to copy.
52
+ const IMPORT_MAP_CDN = {
53
+ vue: 'https://cdn.jsdelivr.net/npm/vue@3.5.22/dist/vue.esm-browser.prod.js',
54
+ '@juuno-sdk/app-sdk': 'https://cdn.jsdelivr.net/npm/@juuno-sdk/app-sdk@1.0.2/dist/index.js',
55
+ '@vueuse/core': 'https://cdn.jsdelivr.net/npm/@vueuse/core@11.4.1/index.mjs',
56
+ '@tiptap/vue-3': 'https://cdn.jsdelivr.net/npm/@tiptap/vue-3@2.10.5/dist/index.js',
57
+ '@tiptap/starter-kit': 'https://cdn.jsdelivr.net/npm/@tiptap/starter-kit@2.10.5/dist/index.js',
58
+ '@tiptap/extension-underline': 'https://cdn.jsdelivr.net/npm/@tiptap/extension-underline@2.10.5/dist/index.js',
59
+ 'lodash-es': 'https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js',
60
+ 'date-fns': 'https://cdn.jsdelivr.net/npm/date-fns@4.1.0/index.mjs',
61
+ 'url-parse': 'https://cdn.jsdelivr.net/npm/url-parse@1.5.10/dist/url-parse.min.js',
62
+ };
63
+ /**
64
+ * Apply the import-map CDN substitutions and the external-app entrypoints
65
+ * to a copy of index.html. Pure over the inputs.
66
+ */
67
+ function applyImportMap(indexHtml, appUrl, playerPath, configPath) {
68
+ let result = indexHtml;
69
+ for (const [key, url] of Object.entries(IMPORT_MAP_CDN)) {
70
+ result = result.replace(`"${key}": ""`, `"${key}": "${url}"`);
71
+ }
72
+ result = result.replace('"external-app/player": ""', `"external-app/player": "${appUrl}${playerPath}"`);
73
+ result = result.replace('"external-app/config": ""', `"external-app/config": "${appUrl}${configPath}"`);
74
+ return result;
75
+ }
76
+ function ensureValidDistDir(distPath) {
59
77
  if (!existsSync(distPath)) {
60
78
  console.error(`Error: Build directory does not exist: ${distPath}`);
61
79
  console.error('Please run "pnpm build" or "pnpm build:simulator" first.');
62
80
  process.exit(1);
63
81
  }
64
- // Validate that dist path is actually a directory.
65
82
  try {
66
83
  const stats = statSync(distPath);
67
84
  if (!stats.isDirectory()) {
@@ -74,44 +91,42 @@ export async function launchSimulatorWithOptions(options) {
74
91
  console.error(`Details: ${error instanceof Error ? error.message : String(error)}`);
75
92
  process.exit(1);
76
93
  }
94
+ }
95
+ function ensureIndexHtmlPresent(indexPath) {
96
+ if (existsSync(indexPath)) {
97
+ return;
98
+ }
99
+ console.error(`Error: index.html not found in build directory after copy`);
100
+ console.error(`Expected location: ${indexPath}`);
101
+ console.error('The build directory may be incomplete or corrupted.');
102
+ console.error('Please run "pnpm build" to rebuild the simulator.');
103
+ process.exit(1);
104
+ }
105
+ /**
106
+ * Launch the app simulator with full options object.
107
+ */
108
+ export async function launchSimulatorWithOptions(options) {
109
+ const { appUrl, port, distPath } = options;
110
+ // Fetch manifest to get actual resource paths.
111
+ const manifest = await fetchManifest(appUrl);
112
+ const playerPath = manifest?.resources?.player?.find((p) => p.endsWith('.js')) || '/player.js';
113
+ const configPath = manifest?.resources?.config?.find((p) => p.endsWith('.js')) || '/config.js';
114
+ ensureValidDistDir(distPath);
77
115
  // Create a temporary directory for the modified dist.
78
116
  const tempDir = join(tmpdir(), `juuno-simulator-${Date.now()}`);
79
117
  mkdirSync(tempDir, { recursive: true });
80
- // Copy all files from dist to temp directory.
81
118
  copyDir(distPath, tempDir);
82
- // Inject the app URL and import map CDN URLs into index.html.
83
119
  const indexPath = join(tempDir, 'index.html');
84
- // Verify index.html exists after copy.
85
- if (!existsSync(indexPath)) {
86
- console.error(`Error: index.html not found in build directory after copy`);
87
- console.error(`Expected location: ${indexPath}`);
88
- console.error('The build directory may be incomplete or corrupted.');
89
- console.error('Please run "pnpm build" to rebuild the simulator.');
90
- process.exit(1);
91
- }
92
- let indexHtml = readFileSync(indexPath, 'utf-8');
93
- // Fill in the import map with CDN URLs for vue and @juuno-sdk/app-sdk.
94
- indexHtml = indexHtml.replace('"vue": ""', '"vue": "https://cdn.jsdelivr.net/npm/vue@3.5.22/dist/vue.esm-browser.prod.js"');
95
- indexHtml = indexHtml.replace('"@juuno-sdk/app-sdk": ""', '"@juuno-sdk/app-sdk": "https://cdn.jsdelivr.net/npm/@juuno-sdk/app-sdk@1.0.2/dist/index.js"');
96
- // Fill in CDN URLs for externalized dependencies from the SDK.
97
- indexHtml = indexHtml.replace('"@vueuse/core": ""', '"@vueuse/core": "https://cdn.jsdelivr.net/npm/@vueuse/core@11.4.1/index.mjs"');
98
- indexHtml = indexHtml.replace('"@tiptap/vue-3": ""', '"@tiptap/vue-3": "https://cdn.jsdelivr.net/npm/@tiptap/vue-3@2.10.5/dist/index.js"');
99
- indexHtml = indexHtml.replace('"@tiptap/starter-kit": ""', '"@tiptap/starter-kit": "https://cdn.jsdelivr.net/npm/@tiptap/starter-kit@2.10.5/dist/index.js"');
100
- indexHtml = indexHtml.replace('"@tiptap/extension-underline": ""', '"@tiptap/extension-underline": "https://cdn.jsdelivr.net/npm/@tiptap/extension-underline@2.10.5/dist/index.js"');
101
- indexHtml = indexHtml.replace('"lodash-es": ""', '"lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js"');
102
- indexHtml = indexHtml.replace('"date-fns": ""', '"date-fns": "https://cdn.jsdelivr.net/npm/date-fns@4.1.0/index.mjs"');
103
- indexHtml = indexHtml.replace('"url-parse": ""', '"url-parse": "https://cdn.jsdelivr.net/npm/url-parse@1.5.10/dist/url-parse.min.js"');
104
- // Fill in the import map with external app entrypoints (from manifest or defaults).
105
- indexHtml = indexHtml.replace('"external-app/player": ""', `"external-app/player": "${appUrl}${playerPath}"`);
106
- indexHtml = indexHtml.replace('"external-app/config": ""', `"external-app/config": "${appUrl}${configPath}"`);
107
- // Inject the app URL as a global variable.
108
- // Use JSON.stringify for safe injection into JavaScript.
120
+ ensureIndexHtmlPresent(indexPath);
121
+ const indexHtmlSource = readFileSync(indexPath, 'utf-8');
122
+ const withImportMap = applyImportMap(indexHtmlSource, appUrl, playerPath, configPath);
123
+ // Inject the app URL as a global variable. JSON.stringify for safe escaping.
109
124
  const appUrlScript = `
110
125
  <script>
111
126
  window.__JUUNO_EXTERNAL_APP_URL__ = ${JSON.stringify(appUrl)};
112
127
  </script>
113
128
  `;
114
- indexHtml = indexHtml.replace('</head>', `${appUrlScript}</head>`);
129
+ const indexHtml = withImportMap.replace('</head>', `${appUrlScript}</head>`);
115
130
  writeFileSync(indexPath, indexHtml);
116
131
  console.log(`
117
132
  🚀 Juuno App Simulator
@@ -0,0 +1,45 @@
1
+ import { getErrorMessage } from './error.js';
2
+ /**
3
+ * Fetch JSON from a developer-API endpoint with bearer-token auth, throwing
4
+ * a useful error on non-2xx responses (status, statusText, body). Used by
5
+ * the info, list, and deploy commands.
6
+ */
7
+ export async function fetchAuthorisedJson(url, token) {
8
+ const response = await fetch(url, {
9
+ method: 'GET',
10
+ headers: {
11
+ Authorization: `Bearer ${token}`,
12
+ Accept: 'application/json',
13
+ },
14
+ });
15
+ if (!response.ok) {
16
+ const body = await response.text();
17
+ throw new Error(`${response.status} ${response.statusText}\n${body}`);
18
+ }
19
+ return (await response.json());
20
+ }
21
+ /**
22
+ * Wrap a thrown error with a contextual prefix. Preserves the original
23
+ * message and lets callers throw a single high-level error with the
24
+ * subsystem context, instead of stringifying details inline at every site.
25
+ */
26
+ export function wrapError(prefix, error) {
27
+ return new Error(`${prefix}: ${getErrorMessage(error)}`);
28
+ }
29
+ /**
30
+ * Validate a developer-API envelope and return the unwrapped data, or
31
+ * throw a specific error for each failure mode (server-reported failure
32
+ * with or without a message; success with missing data).
33
+ */
34
+ export function assertSuccessEnvelope(envelope) {
35
+ if (!envelope.success) {
36
+ if (envelope.message) {
37
+ throw new Error(envelope.message);
38
+ }
39
+ throw new Error('Server returned success=false');
40
+ }
41
+ if (envelope.data === undefined) {
42
+ throw new Error('Server response is missing the data field');
43
+ }
44
+ return envelope.data;
45
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Pull a user-facing string out of an unknown thrown value. Preserves the
3
+ * original Error.message when present; falls back to String() coercion for
4
+ * anything else (which gives e.g. "[object Object]" for plain objects but
5
+ * is still better than crashing the catch site).
6
+ */
7
+ export function getErrorMessage(error) {
8
+ return error instanceof Error ? error.message : String(error);
9
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * True for plain object-shaped values (excludes null and arrays). Use as
3
+ * the entry guard before reading properties on an `unknown` from JSON,
4
+ * dynamic imports, or other untyped boundaries.
5
+ *
6
+ * A copy of `@juuno/utils`'s `isRecord` on purpose: this package publishes
7
+ * standalone and cannot depend on a workspace package.
8
+ */
9
+ export function isRecord(value) {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ }
@@ -1,45 +1,49 @@
1
1
  import { isLoggedIn, getApiUrl, resolveProfile, listProfiles, getProfile, formatProfileFlag, } from '../auth/index.js';
2
- /**
3
- * Shows current authentication status for a profile or all profiles.
4
- */
5
- export async function whoami(options = {}) {
6
- console.log('📦 Juuno CLI - Authentication Status');
7
- console.log('');
8
- if (options.all) {
9
- const profiles = listProfiles();
10
- if (profiles.length === 0) {
11
- console.log('❌ No profiles configured');
12
- console.log('');
13
- console.log('💡 Run "juuno-cli login" to authenticate.');
14
- console.log('');
15
- return;
16
- }
17
- console.log(`✅ ${profiles.length} profile${profiles.length === 1 ? '' : 's'} configured`);
2
+ function printAllProfiles() {
3
+ const profiles = listProfiles();
4
+ if (profiles.length === 0) {
5
+ console.log('❌ No profiles configured');
6
+ console.log('');
7
+ console.log('💡 Run "juuno-cli login" to authenticate.');
18
8
  console.log('');
19
- for (const name of profiles) {
20
- const creds = getProfile(name);
21
- console.log(` ${name}`);
22
- console.log(` API: ${creds?.api_url || 'unknown'}`);
23
- console.log(` Created: ${creds?.created_at || 'unknown'}`);
24
- console.log('');
25
- }
26
9
  return;
27
10
  }
28
- const profile = resolveProfile(options);
29
- const apiUrl = getApiUrl(options);
30
- if (isLoggedIn(profile)) {
31
- console.log(`✅ Logged in (profile: ${profile})`);
32
- console.log('');
33
- console.log(` API: ${apiUrl}`);
34
- console.log(` Credentials: ~/.juuno/credentials.json`);
35
- console.log('');
36
- console.log(`💡 Run "juuno-cli logout${formatProfileFlag(profile)}" to remove stored credentials.`);
11
+ const plural = profiles.length === 1 ? '' : 's';
12
+ console.log(`✅ ${profiles.length} profile${plural} configured`);
13
+ console.log('');
14
+ for (const name of profiles) {
15
+ const creds = getProfile(name);
16
+ console.log(` ${name}`);
17
+ console.log(` API: ${creds?.api_url || 'unknown'}`);
18
+ console.log(` Created: ${creds?.created_at || 'unknown'}`);
37
19
  console.log('');
38
20
  }
39
- else {
21
+ }
22
+ function printSingleProfile(profile, apiUrl) {
23
+ if (!isLoggedIn(profile)) {
40
24
  console.log(`❌ Not logged in (profile: ${profile})`);
41
25
  console.log('');
42
26
  console.log(`💡 Run "juuno-cli login${formatProfileFlag(profile)}" to authenticate.`);
43
27
  console.log('');
28
+ return;
29
+ }
30
+ console.log(`✅ Logged in (profile: ${profile})`);
31
+ console.log('');
32
+ console.log(` API: ${apiUrl}`);
33
+ console.log(` Credentials: ~/.juuno/credentials.json`);
34
+ console.log('');
35
+ console.log(`💡 Run "juuno-cli logout${formatProfileFlag(profile)}" to remove stored credentials.`);
36
+ console.log('');
37
+ }
38
+ /**
39
+ * Shows current authentication status for a profile or all profiles.
40
+ */
41
+ export async function whoami(options = {}) {
42
+ console.log('📦 Juuno CLI - Authentication Status');
43
+ console.log('');
44
+ if (options.all) {
45
+ printAllProfiles();
46
+ return;
44
47
  }
48
+ printSingleProfile(resolveProfile(options), getApiUrl(options));
45
49
  }