@revoengine/cli 1.0.0 → 1.0.2

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 CHANGED
@@ -27,7 +27,9 @@ Log in and save your default credentials:
27
27
  revo auth login
28
28
  ```
29
29
 
30
- `revo auth login` opens an interactive terminal prompt for your API key and instance ID. If you are already logged in, the CLI warns and asks you to `revo auth logout` first.
30
+ `revo auth login` opens an interactive terminal prompt for your API key. If you are already logged in, the CLI warns and asks you to `revo auth logout` first.
31
+
32
+ The CLI validates the API key against `/api/v1/me` and infers the RevoEngine instance from the authenticated profile for tenant-scoped operations such as component push.
31
33
 
32
34
  Check the active session:
33
35
 
@@ -52,10 +54,9 @@ Initialize a project for ambient low-code editor globals:
52
54
  ```bash
53
55
  revo project
54
56
  revo project init ./app
55
- revo project switch a931e927-738d-42a4-b486-5fc19bfb3dfb
56
57
  ```
57
58
 
58
- This writes `.revoengine/types/revo.editor.d.ts`, `.revoengine/revo.json`, patches `tsconfig.json` or `jsconfig.json`, and updates `.gitignore` so the generated type bundle stays local by default. `revo project switch <instanceId>` rebinds the current project to another stored instance and refreshes the ambient editor types for that instance.
59
+ This writes `.revoengine/types/revo.editor.d.ts`, `.revoengine/revo.json`, patches `tsconfig.json` or `jsconfig.json`, and updates `.gitignore` so the generated type bundle stays local by default.
59
60
 
60
61
  ## Common commands
61
62
 
@@ -135,12 +136,14 @@ Components with `category: null` are stored under `Components/__no_category__/..
135
136
  Bulk sync behavior:
136
137
 
137
138
  - `revo component pull --all` and `revo component push --all` require terminal confirmation unless `--force` is passed.
139
+ - `revo component pull --all` requests only active remote components where `deletedAt` is empty.
138
140
  - Pull compares the full local workspace contract before overwriting anything.
139
141
  - Pull skips with `no changes` when the local workspace already matches the remote component.
140
142
  - Pull skips with `changed` when local files differ from the remote contract.
141
143
  - Pull skips with `stale version` when the local version is older than the remote version, unless `--stale` or `--force` is passed.
142
144
  - Push treats backend `Not modified` responses as skipped instead of failing the whole run.
143
- - Debug posts the local `component.json` plus `elements/{order}_{key}.{js|ts}` source files to the instance sandbox `debug` endpoint.
145
+ - Push treats backend `404` responses as skipped with `doesn't exist remotely`; restore the component in RevoEngine before pushing local changes to it.
146
+ - Debug posts the local `component.json` plus `elements/{order}_{key}.{js|ts}` source files to the authenticated sandbox `debug` endpoint.
144
147
  - Sync logs show direction explicitly: `RevoEngine -> path` for pull and `RevoEngine <- path` for push.
145
148
  - Bulk runs print a summary such as `Deployed 54/67, Skipped 13/67 in 13s`.
146
149
 
@@ -148,8 +151,8 @@ Bulk sync behavior:
148
151
 
149
152
  The CLI reads configuration in this order:
150
153
 
151
- 1. CLI flags such as `--url`, `--instance`, and `--token`
152
- 2. Environment variables such as `REVO_URL`, `REVO_INSTANCE`, and `REVO_TOKEN`
154
+ 1. CLI flags such as `--url` and `--token`
155
+ 2. Environment variables such as `REVO_URL` and `REVO_TOKEN`
153
156
  3. Stored config in the OS config directory
154
157
 
155
158
  This means environment variables override the locally stored config, which is the intended setup for CI and non-interactive automation.
@@ -158,7 +161,7 @@ Use `--url` for one-off local or dedicated API targets:
158
161
 
159
162
  ```bash
160
163
  revo --url https://api.test.revong.com -i
161
- revo --url https://api.test.revong.com auth login --instance a931e927-738d-42a4-b486-5fc19bfb3dfb
164
+ revo --url https://api.test.revong.com auth login
162
165
  revo --url https://api.revoengine.com endpoints
163
166
  ```
164
167
 
@@ -167,13 +170,11 @@ Use environment variables for CI/CD or shell profiles:
167
170
  ```bash
168
171
  # TEST
169
172
  export REVO_URL="https://api.test.revong.com"
170
- export REVO_INSTANCE="a931e927-738d-42a4-b486-5fc19bfb3dfb"
171
173
  export REVO_TOKEN="your-test-token"
172
174
  revo endpoints
173
175
 
174
176
  # PROD
175
177
  export REVO_URL="https://api.revoengine.com"
176
- export REVO_INSTANCE="a931e927-738d-42a4-b486-5fc19bfb3dfb"
177
178
  export REVO_TOKEN="your-prod-token"
178
179
  revo endpoints
179
180
  ```
@@ -181,17 +182,15 @@ revo endpoints
181
182
  Supported environment variables:
182
183
 
183
184
  - `REVO_URL` or `REVO_BASE_URL`
184
- - `REVO_INSTANCE`
185
185
  - `REVO_TOKEN` or `REVO_API_KEY`
186
+ - `REVO_NO_UPDATE_NOTIFIER`
186
187
  - `REVOENGINE_URL` or `REVOENGINE_BASE_URL`
187
- - `REVOENGINE_INSTANCE`
188
188
  - `REVOENGINE_TOKEN` or `REVOENGINE_API_KEY`
189
189
 
190
190
  Example for CI:
191
191
 
192
192
  ```bash
193
193
  export REVO_URL="https://api.revoengine.com"
194
- export REVO_INSTANCE="a931e927-738d-42a4-b486-5fc19bfb3dfb"
195
194
  export REVO_TOKEN="your-ci-token"
196
195
  revo endpoints
197
196
  ```
@@ -201,6 +200,22 @@ Stored config lives in:
201
200
  - macOS and Linux: `~/.config/revoengine`
202
201
  - Windows: `%APPDATA%/revoengine`
203
202
 
203
+ ## Update notifications
204
+
205
+ When running in an interactive terminal, the CLI checks the npm registry at most once every 24 hours and prints a colored notice on stderr when a newer `@revoengine/cli` version is available.
206
+
207
+ Disable the check for one run:
208
+
209
+ ```bash
210
+ revo --no-update-notifier endpoints
211
+ ```
212
+
213
+ Disable it through the environment:
214
+
215
+ ```bash
216
+ export REVO_NO_UPDATE_NOTIFIER=1
217
+ ```
218
+
204
219
  ## Local development
205
220
 
206
221
  ```bash
package/dist/src/cli.js CHANGED
@@ -5,6 +5,7 @@ import { resolveRuntimeConfig } from "./config.js";
5
5
  import { handleAuthCommand, handleComponentCommand, handleEndpointsCommand, handleInfoCommand, handleProjectCommand, handleRequestCommand, handleSearchCommand, } from "./commands/index.js";
6
6
  import { buildRuntimeViewModel } from "./runtime-view.js";
7
7
  import { renderHelp, renderOverview } from "./ui.js";
8
+ import { maybeNotifyUpdateAvailable } from "./update-notifier.js";
8
9
  import { readPackageVersion } from "./utils.js";
9
10
  const packageVersion = readPackageVersion();
10
11
  const ANSI = {
@@ -159,6 +160,10 @@ function printHelp() {
159
160
  }
160
161
  export async function main(argv = process.argv.slice(2)) {
161
162
  const args = parseArgs(argv);
163
+ await maybeNotifyUpdateAvailable({
164
+ args,
165
+ currentVersion: packageVersion,
166
+ });
162
167
  const command = args._[0];
163
168
  const noPositionals = args._.length === 0;
164
169
  if (command === 'version' || ((args.version !== undefined || args.V !== undefined) && noPositionals)) {
@@ -35,6 +35,7 @@ export declare class PermissionDeniedError extends ApiError {
35
35
  }
36
36
  declare function buildUrl(baseUrl: string, requestPath: string, query?: Record<string, unknown>): URL;
37
37
  declare function normalizeToken(token?: string): string;
38
+ export declare function extractProfileInstanceId(profile: unknown): string;
38
39
  export declare class RevoClient {
39
40
  baseUrl: string;
40
41
  instance: string;
@@ -66,7 +67,6 @@ export declare class RevoClient {
66
67
  search(params: Record<string, unknown>): Promise<unknown>;
67
68
  listComponents(options?: ComponentListRequest): Promise<unknown>;
68
69
  getComponent(componentId: string): Promise<unknown>;
69
- createComponent(body: Record<string, unknown>): Promise<ApiResponse<unknown>>;
70
70
  saveComponentElements(componentId: string, body: unknown): Promise<ApiResponse<unknown>>;
71
71
  }
72
72
  export { buildUrl, normalizeToken };
@@ -1,4 +1,4 @@
1
- import { AUTH_VALIDATION_TTL_MS, buildAuthValidationKey, DEFAULT_BASE_URL, isUuid, readAuthValidationState, resolveRuntimeConfig, saveAuthValidationState, } from "./config.js";
1
+ import { AUTH_VALIDATION_TTL_MS, buildAuthValidationKey, DEFAULT_BASE_URL, readAuthValidationState, resolveRuntimeConfig, saveAuthValidationState, } from "./config.js";
2
2
  import { createSpinner } from "./spinner.js";
3
3
  export class ApiError extends Error {
4
4
  status;
@@ -125,6 +125,30 @@ async function readResponseData(response) {
125
125
  }
126
126
  return text;
127
127
  }
128
+ function readProfileInstanceId(profile) {
129
+ if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
130
+ return '';
131
+ }
132
+ const record = profile;
133
+ if (typeof record.id === 'string') {
134
+ return record.id;
135
+ }
136
+ const instance = record.instance;
137
+ if (instance && typeof instance === 'object' && !Array.isArray(instance)) {
138
+ const instanceId = instance.id;
139
+ if (typeof instanceId === 'string') {
140
+ return instanceId;
141
+ }
142
+ }
143
+ const data = record.data;
144
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
145
+ return readProfileInstanceId(data);
146
+ }
147
+ return '';
148
+ }
149
+ export function extractProfileInstanceId(profile) {
150
+ return readProfileInstanceId(profile);
151
+ }
128
152
  export class RevoClient {
129
153
  baseUrl;
130
154
  instance;
@@ -133,7 +157,7 @@ export class RevoClient {
133
157
  constructor(options = {}) {
134
158
  const config = resolveRuntimeConfig(options);
135
159
  this.baseUrl = config.baseUrl;
136
- this.instance = config.instance;
160
+ this.instance = config.instance || '';
137
161
  this.token = config.token;
138
162
  this.fetchImpl = options.fetch || globalThis.fetch;
139
163
  }
@@ -154,12 +178,6 @@ export class RevoClient {
154
178
  if (!this.token) {
155
179
  throw new Error('Missing API token. Run `revo auth login` or set REVO_TOKEN / REVO_API_KEY.');
156
180
  }
157
- if (!this.instance) {
158
- throw new Error('Missing instance UUID. Run `revo auth login` or set REVO_INSTANCE.');
159
- }
160
- if (!isUuid(this.instance)) {
161
- throw new Error(`Invalid instance UUID: ${this.instance}`);
162
- }
163
181
  }
164
182
  async validateSession(options = {}) {
165
183
  this.assertReady();
@@ -173,6 +191,7 @@ export class RevoClient {
173
191
  throw new AuthenticationError(401, 'Not authenticated. Run `revo auth login`.', null);
174
192
  }
175
193
  if (cached.profile !== undefined) {
194
+ this.instance = this.instance || extractProfileInstanceId(cached.profile);
176
195
  return {
177
196
  authenticated: true,
178
197
  fromCache: true,
@@ -182,6 +201,7 @@ export class RevoClient {
182
201
  }
183
202
  try {
184
203
  const profile = await this.requestData('GET', '/api/v1/me', { authGuard: false });
204
+ this.instance = this.instance || extractProfileInstanceId(profile);
185
205
  saveAuthValidationState({
186
206
  key: this.authValidationKey,
187
207
  status: 'authenticated',
@@ -215,8 +235,10 @@ export class RevoClient {
215
235
  const headers = new Headers(options.headers || {});
216
236
  headers.set('Authorization', this.authHeader);
217
237
  headers.set('x-api-key', this.token);
218
- headers.set('instance', this.instance);
219
- headers.set('x-api-instance', this.instance);
238
+ if (this.instance) {
239
+ headers.set('instance', this.instance);
240
+ headers.set('x-api-instance', this.instance);
241
+ }
220
242
  let body = options.body;
221
243
  if (body !== undefined && body !== null && method.toUpperCase() !== 'GET') {
222
244
  headers.set('Content-Type', 'application/json');
@@ -305,9 +327,6 @@ export class RevoClient {
305
327
  async getComponent(componentId) {
306
328
  return this.requestData('GET', `/api/v1/component/${componentId}`);
307
329
  }
308
- async createComponent(body) {
309
- return this.request('POST', '/api/v1/component', { body });
310
- }
311
330
  async saveComponentElements(componentId, body) {
312
331
  return this.request('POST', `/api/v1/component/${componentId}/save`, { body });
313
332
  }
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { clearAuthValidationState, clearStoredConfig, getConfigDir, isUuid, loadStoredConfig, resolveRuntimeConfig, saveStoredConfig, } from "../config.js";
2
+ import { clearAuthValidationState, clearStoredConfig, getConfigDir, loadStoredConfig, resolveRuntimeConfig, saveStoredConfig, } from "../config.js";
3
3
  import { readLegacyImportFromFile } from "../legacy.js";
4
4
  import { promptText } from "../prompt.js";
5
5
  import { buildRuntimeViewModel } from "../runtime-view.js";
@@ -8,28 +8,19 @@ import { readBoolFlag, readFlag } from "../utils.js";
8
8
  function getConfigInput(args) {
9
9
  return resolveRuntimeConfig({
10
10
  baseUrl: readFlag(args, ['url', 'baseUrl']) || '',
11
- instance: readFlag(args, ['instance', 'i']) || '',
12
11
  token: readFlag(args, ['token', 't']) || '',
13
12
  });
14
13
  }
15
14
  async function promptMissingConfig(runtime) {
16
15
  let token = runtime.token;
17
- let instance = runtime.instance;
18
16
  if (!token) {
19
17
  token = (await promptText('Paste API key (input is visible)')).trim();
20
18
  }
21
- if (!instance) {
22
- instance = (await promptText('Paste instance ID')).trim();
23
- }
24
19
  if (!token) {
25
20
  throw new Error('API key is required.');
26
21
  }
27
- if (!instance || !isUuid(instance)) {
28
- throw new Error('Instance ID must be a UUID.');
29
- }
30
22
  return {
31
23
  token,
32
- instance,
33
24
  baseUrl: runtime.baseUrl,
34
25
  };
35
26
  }
@@ -44,7 +35,6 @@ export async function handleAuthCommand(context) {
44
35
  if (subcommand === 'get') {
45
36
  const stored = loadStoredConfig();
46
37
  print({
47
- instance: stored.instance || null,
48
38
  baseUrl: stored.baseUrl,
49
39
  tokenConfigured: Boolean(stored.token),
50
40
  configDir: getConfigDir(),
@@ -52,21 +42,7 @@ export async function handleAuthCommand(context) {
52
42
  return;
53
43
  }
54
44
  if (subcommand === 'set') {
55
- const instance = readFlag(args, ['instance', 'i']);
56
- if (!instance) {
57
- throw new Error('Missing instance UUID.');
58
- }
59
- if (!isUuid(instance)) {
60
- throw new Error('Instance must be a UUID.');
61
- }
62
- const stored = loadStoredConfig();
63
- saveStoredConfig({
64
- baseUrl: stored.baseUrl,
65
- token: stored.token,
66
- instance,
67
- });
68
- println(`Saved default instance ${instance}.`);
69
- return;
45
+ throw new Error('`revo auth set` was removed because API-key credentials are no longer instance-scoped.');
70
46
  }
71
47
  if (subcommand === 'status') {
72
48
  const view = await buildRuntimeViewModel(context, { forceValidation: true });
@@ -88,7 +64,7 @@ export async function handleAuthCommand(context) {
88
64
  client.token = imported.token;
89
65
  client.instance = imported.instance;
90
66
  client.baseUrl = imported.baseUrl;
91
- if (imported.token && imported.instance) {
67
+ if (imported.token) {
92
68
  try {
93
69
  await client.validateSession({ force: true });
94
70
  }
@@ -105,14 +81,13 @@ export async function handleAuthCommand(context) {
105
81
  throw new Error('Unknown auth command.');
106
82
  }
107
83
  const stored = loadStoredConfig();
108
- if (stored.token && stored.instance) {
109
- println(`Warning: already logged in for instance ${stored.instance}. Run \`revo auth logout\` before logging in again.`);
84
+ if (stored.token) {
85
+ println('Warning: already logged in. Run `revo auth logout` before logging in again.');
110
86
  return;
111
87
  }
112
88
  const runtime = getConfigInput(args);
113
- const next = runtime.token && runtime.instance ? runtime : await promptMissingConfig(runtime);
89
+ const next = runtime.token ? runtime : await promptMissingConfig(runtime);
114
90
  client.token = next.token;
115
- client.instance = next.instance;
116
91
  client.baseUrl = next.baseUrl;
117
92
  try {
118
93
  await client.validateSession({ force: true });
@@ -125,7 +100,10 @@ export async function handleAuthCommand(context) {
125
100
  error(`Saved credentials, but validation failed: ${String(validationError)}`);
126
101
  return;
127
102
  }
128
- saveStoredConfig(next);
103
+ saveStoredConfig({
104
+ ...next,
105
+ instance: client.instance || undefined,
106
+ });
129
107
  clearAuthValidationState();
130
108
  println(`Logged in and saved credentials to ${getConfigDir()}.`);
131
109
  }
@@ -5,7 +5,11 @@ import { buildSandboxDebugUrl, extractSandboxEndpoint } from "../project.js";
5
5
  import { isInteractiveTerminal, promptConfirm } from "../prompt.js";
6
6
  import { deepClone, readBoolFlag, readFlag, readValues, sanitizeSegment, writeJsonFile } from "../utils.js";
7
7
  const NULL_CATEGORY_FOLDER = '__no_category__';
8
- const COMPONENT_LIST_PAGE_SIZE = 100;
8
+ const COMPONENT_LIST_PAGE_SIZE = 200;
9
+ const ACTIVE_COMPONENT_LIST_FILTER = {
10
+ 'filter[and][0][field]': 'deletedAt',
11
+ 'filter[and][0][op]': 'isNull',
12
+ };
9
13
  const ANSI = {
10
14
  reset: '\u001b[0m',
11
15
  bold: '\u001b[1m',
@@ -202,23 +206,6 @@ function getComponentFolder(component) {
202
206
  const componentId = sanitizeSegment(component.componentId || component.id || 'unknown');
203
207
  return path.join(categoryFolder, `${name}-${componentId}`);
204
208
  }
205
- function getCategoryFromManifestPath(manifestPath) {
206
- const componentDir = path.dirname(manifestPath);
207
- const categoryDir = path.basename(path.dirname(componentDir));
208
- if (categoryDir === NULL_CATEGORY_FOLDER) {
209
- return null;
210
- }
211
- return categoryDir;
212
- }
213
- function normalizeCategoryForApi(manifestPath, category) {
214
- if (category === NULL_CATEGORY_FOLDER) {
215
- return null;
216
- }
217
- if (category != null) {
218
- return category;
219
- }
220
- return getCategoryFromManifestPath(manifestPath);
221
- }
222
209
  function getDetailsExtension(component) {
223
210
  return componentTypeToExtension(normalizeComponentType(component));
224
211
  }
@@ -351,6 +338,27 @@ function resolveNextComponentListRequest(value) {
351
338
  }
352
339
  return Object.keys(query).length > 0 ? { query } : null;
353
340
  }
341
+ function buildActiveComponentListQuery(skip, take = COMPONENT_LIST_PAGE_SIZE) {
342
+ return {
343
+ take,
344
+ skip,
345
+ count: true,
346
+ ...ACTIVE_COMPONENT_LIST_FILTER,
347
+ };
348
+ }
349
+ function withActiveComponentListFilter(request) {
350
+ if (!request.query) {
351
+ return request;
352
+ }
353
+ return {
354
+ ...request,
355
+ query: {
356
+ ...request.query,
357
+ count: request.query.count ?? true,
358
+ ...ACTIVE_COMPONENT_LIST_FILTER,
359
+ },
360
+ };
361
+ }
354
362
  function unwrapComponentListPage(value) {
355
363
  const items = unwrapList(value);
356
364
  if (!isRecord(value)) {
@@ -571,10 +579,7 @@ async function pullAllComponents(context, options) {
571
579
  const components = [];
572
580
  const seenRequests = new Set();
573
581
  let nextRequest = {
574
- query: {
575
- take: COMPONENT_LIST_PAGE_SIZE,
576
- skip: 0,
577
- },
582
+ query: buildActiveComponentListQuery(0),
578
583
  };
579
584
  let discoveredTotal = null;
580
585
  while (nextRequest) {
@@ -604,17 +609,14 @@ async function pullAllComponents(context, options) {
604
609
  continue;
605
610
  }
606
611
  if (page.nextRequest) {
607
- nextRequest = page.nextRequest;
612
+ nextRequest = withActiveComponentListFilter(page.nextRequest);
608
613
  continue;
609
614
  }
610
615
  if (nextRequest.query
611
616
  && typeof nextRequest.query.take === 'number'
612
617
  && page.items.length === nextRequest.query.take) {
613
618
  nextRequest = {
614
- query: {
615
- take: nextRequest.query.take,
616
- skip: components.length,
617
- },
619
+ query: buildActiveComponentListQuery(components.length, nextRequest.query.take),
618
620
  };
619
621
  continue;
620
622
  }
@@ -655,15 +657,6 @@ async function pushSingleComponent(context, manifestPath) {
655
657
  if (!component.name) {
656
658
  throw new Error(`Missing component name in ${manifestPath}.`);
657
659
  }
658
- const payload = {
659
- componentId,
660
- name: component.name,
661
- category: normalizeCategoryForApi(manifestPath, component.category),
662
- desc: component.desc,
663
- type: normalizeComponentType(component),
664
- active: component.active ?? true,
665
- async: component.async ?? false,
666
- };
667
660
  const elements = (component.elements || []).map((element) => ({
668
661
  key: element.key,
669
662
  desc: element.desc,
@@ -717,21 +710,16 @@ async function pushSingleComponent(context, manifestPath) {
717
710
  throw new Error('Access denied (403). You are authenticated, but you do not have permission to push components.');
718
711
  }
719
712
  if (error instanceof ApiError && error.status === 404) {
720
- try {
721
- await client.createComponent(payload);
722
- await client.saveComponentElements(componentId, elements);
723
- }
724
- catch (createError) {
725
- rethrowComponentAccessError(createError, 'push');
726
- }
727
713
  const result = {
728
- status: 'deployed',
714
+ status: 'skipped',
729
715
  targetPath,
716
+ reason: "doesn't exist remotely",
730
717
  };
731
718
  printSyncStatus(println, {
732
- status: 'Deployed',
719
+ status: 'Skipped',
733
720
  direction: 'push',
734
721
  targetPath,
722
+ reason: result.reason,
735
723
  });
736
724
  return result;
737
725
  }
@@ -837,7 +825,7 @@ async function debugSingleComponent(context, componentId) {
837
825
  const profile = await context.client.me();
838
826
  const sandboxEndpoint = extractSandboxEndpoint(profile);
839
827
  if (!sandboxEndpoint) {
840
- throw new Error('Authenticated profile did not include `endpoints.sandbox`, so component debug cannot run for this instance.');
828
+ throw new Error('Authenticated profile did not include `endpoints.sandbox`, so component debug cannot run.');
841
829
  }
842
830
  const response = await context.client.debugComponent(buildSandboxDebugUrl(sandboxEndpoint), {
843
831
  elements: buildDebugElements(component),
@@ -1,6 +1,5 @@
1
1
  import path from 'node:path';
2
- import { isUuid, loadStoredConfigIndex } from "../config.js";
3
- import { buildProjectSyncState, buildEditorTypesUrl, extractSandboxEndpoint, extractEditorTypesBundle, findProjectMetadataFile, resolveProjectInvocation, resolveProjectRoot, resolveProjectTarget, syncProjectFiles, } from "../project.js";
2
+ import { buildProjectSyncState, buildEditorTypesUrl, extractSandboxEndpoint, extractEditorTypesBundle, resolveProjectInvocation, resolveProjectTarget, syncProjectFiles, } from "../project.js";
4
3
  function printSyncSummary(println, targetDir, result, prefix) {
5
4
  println(`${prefix} ${targetDir}`);
6
5
  println(`Wrote ${path.relative(targetDir, result.typesFile)}`);
@@ -20,12 +19,11 @@ async function resolveProjectSyncInput(context) {
20
19
  const profile = await client.me();
21
20
  const sandboxEndpoint = extractSandboxEndpoint(profile);
22
21
  if (!sandboxEndpoint) {
23
- throw new Error('Authenticated profile did not include `endpoints.sandbox`, so editor types cannot be initialized for this instance.');
22
+ throw new Error('Authenticated profile did not include `endpoints.sandbox`, so editor types cannot be initialized.');
24
23
  }
25
24
  const bundle = extractEditorTypesBundle(await client.getEditorTypes(buildEditorTypesUrl(sandboxEndpoint)));
26
25
  return buildProjectSyncState({
27
26
  fallbackEndpoint: sandboxEndpoint,
28
- fallbackInstanceId: client.instance,
29
27
  bundle,
30
28
  });
31
29
  }
@@ -35,41 +33,6 @@ export async function handleProjectCommand(context) {
35
33
  if (invocation.action === 'update') {
36
34
  throw new Error('`revo project update` is not implemented yet.');
37
35
  }
38
- if (invocation.action === 'switch') {
39
- if (invocation.extraArgs.length > 0) {
40
- throw new Error('Project switch accepts exactly one instance ID.');
41
- }
42
- const instanceId = invocation.targetArg || '';
43
- if (!instanceId) {
44
- throw new Error('Missing instance UUID. Usage: `revo project switch <instanceId>`.');
45
- }
46
- if (!isUuid(instanceId)) {
47
- throw new Error('Instance ID must be a UUID.');
48
- }
49
- const metadataFile = findProjectMetadataFile(cwd);
50
- if (!metadataFile) {
51
- throw new Error('Current directory is not inside an initialized Revo project.');
52
- }
53
- const projectRoot = resolveProjectRoot(cwd);
54
- if (!projectRoot) {
55
- throw new Error('Unable to resolve the current Revo project root.');
56
- }
57
- const storedConfigs = loadStoredConfigIndex();
58
- const selected = storedConfigs.instances[instanceId];
59
- if (!selected) {
60
- throw new Error(`No stored credentials found for instance ${instanceId}. Run \`revo auth login --instance ${instanceId}\` first.`);
61
- }
62
- if (!selected.token) {
63
- throw new Error(`Stored credentials for instance ${instanceId} are incomplete. Run \`revo auth login --instance ${instanceId}\` again.`);
64
- }
65
- client.baseUrl = selected.baseUrl;
66
- client.instance = instanceId;
67
- client.token = selected.token;
68
- const syncInput = await resolveProjectSyncInput(context);
69
- const result = syncProjectFiles(projectRoot, syncInput);
70
- printSyncSummary(println, projectRoot, result, `Switched Revo project to ${instanceId} in`);
71
- return;
72
- }
73
36
  if (invocation.extraArgs.length > 0) {
74
37
  throw new Error('Project init accepts at most one path argument.');
75
38
  }
@@ -10,14 +10,8 @@ export type RuntimeConfigOptions = {
10
10
  instance?: string;
11
11
  token?: string;
12
12
  };
13
- type StoredConfig = RuntimeConfig;
14
- type StoredInstanceConfig = {
15
- baseUrl: string;
16
- token: string;
17
- };
18
- type StoredConfigFile = {
19
- defaultInstance: string;
20
- instances: Record<string, StoredInstanceConfig>;
13
+ type StoredConfig = Omit<RuntimeConfig, 'instance'> & {
14
+ instance?: string;
21
15
  };
22
16
  export type AuthValidationState = {
23
17
  key: string;
@@ -37,11 +31,20 @@ export declare function readProjectInstanceId(startDir?: string): string;
37
31
  export declare function isUuid(value: unknown): value is string;
38
32
  export declare function saveStoredConfig(nextConfig: Partial<StoredConfig>): void;
39
33
  export declare function clearStoredConfig(): void;
40
- export declare function loadStoredConfigIndex(): StoredConfigFile;
34
+ export declare function loadStoredConfigIndex(): {
35
+ defaultInstance: string;
36
+ instances: {
37
+ default: {
38
+ baseUrl: string;
39
+ token: string;
40
+ };
41
+ } | {
42
+ default?: undefined;
43
+ };
44
+ };
41
45
  export declare function loadStoredConfig(options?: {
42
- instance?: string;
43
46
  allowLegacy?: boolean;
44
- }): RuntimeConfig;
47
+ }): StoredConfig;
45
48
  export declare function buildAuthValidationKey(runtime: RuntimeConfigOptions): string;
46
49
  export declare function readAuthValidationState(key?: string): AuthValidationState;
47
50
  export declare function saveAuthValidationState(state: AuthValidationState): void;
@@ -73,20 +73,13 @@ export function saveStoredConfig(nextConfig) {
73
73
  const { dir, configFile, credentialsFile } = getConfigPaths();
74
74
  ensureDirectory(dir);
75
75
  const current = readStoredConfigFile();
76
- const fallback = current.defaultInstance ? current.instances[current.defaultInstance] : undefined;
77
- const instance = nextConfig.instance || current.defaultInstance || '';
78
- if (!instance) {
79
- writeJsonFile(configFile, current);
80
- removeFileIfExists(credentialsFile);
81
- return;
82
- }
83
- const currentEntry = current.instances[instance];
84
- current.instances[instance] = {
85
- baseUrl: nextConfig.baseUrl || currentEntry?.baseUrl || fallback?.baseUrl || DEFAULT_BASE_URL,
86
- token: nextConfig.token || currentEntry?.token || fallback?.token || '',
76
+ const instance = nextConfig.instance || current.instance || '';
77
+ const config = {
78
+ baseUrl: nextConfig.baseUrl || current.baseUrl || DEFAULT_BASE_URL,
79
+ token: nextConfig.token || current.token || '',
80
+ ...(instance ? { instance } : {}),
87
81
  };
88
- current.defaultInstance = instance;
89
- writeJsonFile(configFile, current);
82
+ writeJsonFile(configFile, config);
90
83
  removeFileIfExists(credentialsFile);
91
84
  }
92
85
  export function clearStoredConfig() {
@@ -131,8 +124,8 @@ function migrateStoredConfigBestEffort(configFile, credentialsFile, nextConfig)
131
124
  }
132
125
  function emptyStoredConfigFile() {
133
126
  return {
134
- defaultInstance: '',
135
- instances: {},
127
+ baseUrl: DEFAULT_BASE_URL,
128
+ token: '',
136
129
  };
137
130
  }
138
131
  function isRecord(value) {
@@ -151,6 +144,25 @@ function normalizeInstanceConfig(value) {
151
144
  token: typeof value.token === 'string' ? value.token : '',
152
145
  };
153
146
  }
147
+ function parseFlatConfig(raw, credentials) {
148
+ if (!isRecord(raw) || isRecord(raw.instances)) {
149
+ return null;
150
+ }
151
+ const token = typeof raw.token === 'string'
152
+ ? raw.token
153
+ : isRecord(credentials) && typeof credentials.token === 'string'
154
+ ? credentials.token
155
+ : '';
156
+ return {
157
+ baseUrl: typeof raw.baseUrl === 'string'
158
+ ? raw.baseUrl
159
+ : typeof raw.url === 'string'
160
+ ? raw.url
161
+ : DEFAULT_BASE_URL,
162
+ ...(typeof raw.instance === 'string' && isUuid(raw.instance) ? { instance: raw.instance } : {}),
163
+ token,
164
+ };
165
+ }
154
166
  function parseMappedConfig(raw) {
155
167
  if (!isRecord(raw) || !isRecord(raw.instances)) {
156
168
  return null;
@@ -169,10 +181,14 @@ function parseMappedConfig(raw) {
169
181
  const defaultInstance = isUuid(configuredDefault) && instances[configuredDefault]
170
182
  ? configuredDefault
171
183
  : Object.keys(instances)[0] || '';
172
- return {
173
- defaultInstance,
174
- instances,
175
- };
184
+ const selected = defaultInstance ? instances[defaultInstance] : undefined;
185
+ return selected
186
+ ? {
187
+ baseUrl: selected.baseUrl,
188
+ instance: defaultInstance,
189
+ token: selected.token,
190
+ }
191
+ : emptyStoredConfigFile();
176
192
  }
177
193
  function parseLegacyConfig(config, credentials) {
178
194
  const baseUrl = isRecord(config) && typeof config.baseUrl === 'string'
@@ -180,58 +196,59 @@ function parseLegacyConfig(config, credentials) {
180
196
  : isRecord(config) && typeof config.url === 'string'
181
197
  ? config.url
182
198
  : DEFAULT_BASE_URL;
183
- const instance = isRecord(config) && typeof config.instance === 'string' ? config.instance : '';
184
199
  const token = isRecord(credentials) && typeof credentials.token === 'string' ? credentials.token : '';
185
- if (!instance) {
186
- return emptyStoredConfigFile();
187
- }
188
200
  return {
189
- defaultInstance: instance,
190
- instances: {
191
- [instance]: {
192
- baseUrl,
193
- token,
194
- },
195
- },
201
+ baseUrl,
202
+ ...(isRecord(config) && typeof config.instance === 'string' && isUuid(config.instance)
203
+ ? { instance: config.instance }
204
+ : {}),
205
+ token,
196
206
  };
197
207
  }
198
208
  function readStoredConfigFile() {
199
209
  const { configFile, credentialsFile } = getConfigPaths();
200
210
  const config = fs.existsSync(configFile) ? readJsonFile(configFile) : {};
201
211
  const credentials = fs.existsSync(credentialsFile) ? readJsonFile(credentialsFile) : {};
202
- const mapped = parseMappedConfig(config);
203
- if (mapped) {
212
+ const flat = parseFlatConfig(config, credentials);
213
+ if (flat) {
204
214
  if (fs.existsSync(credentialsFile)) {
205
- migrateStoredConfigBestEffort(configFile, credentialsFile, mapped);
215
+ migrateStoredConfigBestEffort(configFile, credentialsFile, flat);
206
216
  }
217
+ return flat;
218
+ }
219
+ const mapped = parseMappedConfig(config);
220
+ if (mapped) {
221
+ migrateStoredConfigBestEffort(configFile, credentialsFile, mapped);
207
222
  return mapped;
208
223
  }
209
224
  const legacy = parseLegacyConfig(config, credentials);
210
- if (legacy.defaultInstance) {
225
+ if (legacy.token || legacy.baseUrl !== DEFAULT_BASE_URL) {
211
226
  migrateStoredConfigBestEffort(configFile, credentialsFile, legacy);
212
227
  }
213
228
  return legacy;
214
229
  }
215
230
  export function loadStoredConfigIndex() {
216
- return readStoredConfigFile();
217
- }
218
- function readStoredConfig(options = {}) {
219
231
  const stored = readStoredConfigFile();
220
- const selectedInstance = options.instance && stored.instances[options.instance]
221
- ? options.instance
222
- : stored.defaultInstance;
223
- const selected = selectedInstance ? stored.instances[selectedInstance] : undefined;
224
232
  return {
225
- baseUrl: selected?.baseUrl || DEFAULT_BASE_URL,
226
- instance: selectedInstance || '',
227
- token: selected?.token || '',
233
+ defaultInstance: '',
234
+ instances: stored.token
235
+ ? {
236
+ default: {
237
+ baseUrl: stored.baseUrl,
238
+ token: stored.token,
239
+ },
240
+ }
241
+ : {},
228
242
  };
229
243
  }
244
+ function readStoredConfig() {
245
+ return readStoredConfigFile();
246
+ }
230
247
  function mergeWithLegacy(stored, options = {}) {
231
248
  if (options.allowLegacy === false) {
232
249
  return stored;
233
250
  }
234
- if ((stored.token && stored.instance) || !process.cwd()) {
251
+ if (stored.token || !process.cwd()) {
235
252
  return stored;
236
253
  }
237
254
  const legacy = readLegacyImportFromProject(process.cwd());
@@ -240,16 +257,15 @@ function mergeWithLegacy(stored, options = {}) {
240
257
  }
241
258
  const merged = {
242
259
  baseUrl: stored.baseUrl || legacy.baseUrl || DEFAULT_BASE_URL,
243
- instance: stored.instance || options.instance || legacy.instance || '',
244
260
  token: stored.token || legacy.token || '',
245
261
  };
246
- if (merged.token || merged.instance || merged.baseUrl !== DEFAULT_BASE_URL) {
262
+ if (merged.token || merged.baseUrl !== DEFAULT_BASE_URL) {
247
263
  saveStoredConfig(merged);
248
264
  }
249
265
  return merged;
250
266
  }
251
267
  export function loadStoredConfig(options = {}) {
252
- return mergeWithLegacy(readStoredConfig(options), options);
268
+ return mergeWithLegacy(readStoredConfig(), options);
253
269
  }
254
270
  export function buildAuthValidationKey(runtime) {
255
271
  return createHash('sha256')
@@ -342,12 +358,7 @@ export function resolveRuntimeConfig(options = {}) {
342
358
  instance: resolveEnvValue(['REVO_INSTANCE', 'REVOENGINE_INSTANCE']),
343
359
  token: resolveEnvValue(['REVO_TOKEN', 'REVO_API_KEY', 'REVOENGINE_TOKEN', 'REVOENGINE_API_KEY']),
344
360
  };
345
- const projectInstance = !options.instance && !env.instance ? readProjectInstanceId() : '';
346
- const requestedInstance = options.instance || env.instance || projectInstance || undefined;
347
- const stored = loadStoredConfig({
348
- instance: requestedInstance,
349
- allowLegacy: !requestedInstance,
350
- });
361
+ const stored = loadStoredConfig();
351
362
  return {
352
363
  baseUrl: options.baseUrl || env.baseUrl || stored.baseUrl || DEFAULT_BASE_URL,
353
364
  instance: options.instance || env.instance || stored.instance || '',
@@ -11,14 +11,13 @@ export declare const REVO_DEBUG_CONFIG_FILE: string;
11
11
  export declare const REVO_PLAYGROUND_JS_FILE: string;
12
12
  export declare const REVO_PLAYGROUND_TS_FILE: string;
13
13
  export type ProjectInvocation = {
14
- action: 'init' | 'switch' | 'update';
14
+ action: 'init' | 'update';
15
15
  targetArg?: string;
16
16
  extraArgs: string[];
17
17
  };
18
18
  export type EditorTypesBundle = {
19
19
  code: string;
20
20
  endpoint?: string;
21
- instanceId?: string;
22
21
  apiVersion?: string;
23
22
  libVersion?: string;
24
23
  hash?: string;
@@ -27,7 +26,6 @@ export type EditorTypesBundle = {
27
26
  export type RevoProjectMetadata = {
28
27
  schemaVersion: 1;
29
28
  endpoint: string;
30
- instanceId: string;
31
29
  authProfile: string;
32
30
  apiVersion: string;
33
31
  libVersion: string;
@@ -36,7 +34,6 @@ export type RevoProjectMetadata = {
36
34
  };
37
35
  export type ProjectSyncInput = {
38
36
  endpoint: string;
39
- instanceId: string;
40
37
  code: string;
41
38
  apiVersion: string;
42
39
  libVersion: string;
@@ -60,13 +57,11 @@ export declare function buildSandboxDebugUrl(endpoint: string): string;
60
57
  export declare function extractSandboxEndpoint(profile: unknown): string | null;
61
58
  export declare function buildProjectMetadata(input: {
62
59
  baseUrl: string;
63
- instance: string;
64
60
  bundle: EditorTypesBundle;
65
61
  now?: Date;
66
62
  }): {
67
63
  schemaVersion: 1;
68
64
  endpoint: string;
69
- instanceId: string;
70
65
  authProfile: string;
71
66
  apiVersion: string;
72
67
  libVersion: string;
@@ -78,12 +73,10 @@ export declare function readProjectMetadata(startDir?: string): RevoProjectMetad
78
73
  export declare function resolveProjectRoot(startDir?: string): string;
79
74
  export declare function buildProjectSyncState(input: {
80
75
  fallbackEndpoint: string;
81
- fallbackInstanceId: string;
82
76
  bundle: EditorTypesBundle;
83
77
  now?: Date;
84
78
  }): {
85
79
  endpoint: string;
86
- instanceId: string;
87
80
  code: string;
88
81
  apiVersion: string;
89
82
  libVersion: string;
@@ -192,13 +192,16 @@ export function resolveProjectInvocation(args) {
192
192
  extraArgs: [],
193
193
  };
194
194
  }
195
- if (first === 'init' || first === 'switch' || first === 'update') {
195
+ if (first === 'init' || first === 'update') {
196
196
  return {
197
197
  action: first,
198
198
  targetArg: second,
199
199
  extraArgs: rest,
200
200
  };
201
201
  }
202
+ if (first === 'switch') {
203
+ throw new Error('`revo project switch` was removed because credentials are no longer instance-scoped.');
204
+ }
202
205
  return {
203
206
  action: 'init',
204
207
  targetArg: first,
@@ -221,7 +224,6 @@ export function extractEditorTypesBundle(payload) {
221
224
  return {
222
225
  code,
223
226
  endpoint: readString(root.endpoint) || readString(meta?.endpoint),
224
- instanceId: readString(root.instanceId) || readString(root.instance) || readString(meta?.instanceId) || readString(meta?.instance),
225
227
  apiVersion: readString(root.apiVersion) || readString(meta?.apiVersion),
226
228
  libVersion: readString(root.libVersion) || readString(meta?.libVersion),
227
229
  hash: readString(root.hash) || readString(meta?.hash),
@@ -265,27 +267,27 @@ export function extractSandboxEndpoint(profile) {
265
267
  roots.push(nestedProfile);
266
268
  }
267
269
  for (const root of roots) {
268
- const instance = root.instance;
269
- if (!instance || typeof instance !== 'object' || Array.isArray(instance)) {
270
- continue;
271
- }
272
- const endpoints = instance.endpoints;
273
- if (!endpoints || typeof endpoints !== 'object' || Array.isArray(endpoints)) {
274
- continue;
275
- }
276
- const sandbox = endpoints.sandbox;
277
- if (typeof sandbox === 'string' && sandbox) {
278
- return normalizeEndpoint(sandbox);
270
+ const endpointContainers = [
271
+ root.endpoints,
272
+ isRecord(root.instance) ? root.instance.endpoints : null,
273
+ ];
274
+ for (const endpoints of endpointContainers) {
275
+ if (!isRecord(endpoints)) {
276
+ continue;
277
+ }
278
+ const sandbox = endpoints.sandbox;
279
+ if (typeof sandbox === 'string' && sandbox) {
280
+ return normalizeEndpoint(sandbox);
281
+ }
279
282
  }
280
283
  }
281
284
  return null;
282
285
  }
283
286
  export function buildProjectMetadata(input) {
284
- const { baseUrl, bundle, instance, now = new Date() } = input;
287
+ const { baseUrl, bundle, now = new Date() } = input;
285
288
  return {
286
289
  schemaVersion: 1,
287
290
  endpoint: bundle.endpoint || buildEditorEndpoint(baseUrl),
288
- instanceId: bundle.instanceId || instance,
289
291
  authProfile: 'default',
290
292
  apiVersion: getRequiredMetadataValue(bundle, 'apiVersion'),
291
293
  libVersion: getRequiredMetadataValue(bundle, 'libVersion'),
@@ -319,8 +321,7 @@ export function readProjectMetadata(startDir = process.cwd()) {
319
321
  if (!isRecord(raw)) {
320
322
  return null;
321
323
  }
322
- if (typeof raw.instanceId !== 'string'
323
- || typeof raw.endpoint !== 'string'
324
+ if (typeof raw.endpoint !== 'string'
324
325
  || typeof raw.authProfile !== 'string'
325
326
  || typeof raw.apiVersion !== 'string'
326
327
  || typeof raw.libVersion !== 'string'
@@ -337,13 +338,11 @@ export function resolveProjectRoot(startDir = process.cwd()) {
337
338
  export function buildProjectSyncState(input) {
338
339
  const metadata = buildProjectMetadata({
339
340
  baseUrl: input.fallbackEndpoint,
340
- instance: input.fallbackInstanceId,
341
341
  bundle: input.bundle,
342
342
  now: input.now,
343
343
  });
344
344
  return {
345
345
  endpoint: metadata.endpoint,
346
- instanceId: metadata.instanceId,
347
346
  code: input.bundle.code,
348
347
  apiVersion: metadata.apiVersion,
349
348
  libVersion: metadata.libVersion,
@@ -362,7 +361,6 @@ export function syncProjectFiles(targetDir, input) {
362
361
  writeJsonFile(metadataFile, {
363
362
  schemaVersion: 1,
364
363
  endpoint: input.endpoint,
365
- instanceId: input.instanceId,
366
364
  authProfile: 'default',
367
365
  apiVersion: input.apiVersion,
368
366
  libVersion: input.libVersion,
@@ -49,9 +49,9 @@ export async function buildRuntimeViewModel(context, options = {}) {
49
49
  validation: null,
50
50
  email: null,
51
51
  };
52
- if (runtime.token && runtime.instance) {
52
+ if (runtime.token) {
53
53
  context.client.baseUrl = runtime.baseUrl;
54
- context.client.instance = runtime.instance;
54
+ context.client.instance = runtime.instance || '';
55
55
  context.client.token = runtime.token;
56
56
  try {
57
57
  const profile = await context.client.me({ force: options.forceValidation });
package/dist/src/ui.js CHANGED
@@ -95,16 +95,15 @@ function renderCommandCatalog() {
95
95
  '',
96
96
  paintHeader('Global Options'),
97
97
  commandRow('--url <url>', 'Override API base URL for TEST, prod, local, or dedicated tenants'),
98
- commandRow('--instance <uuid>', 'Override instance ID'),
99
98
  commandRow('--token <apiKey>', 'Override API key for non-interactive runs'),
99
+ commandRow('--no-update-notifier', 'Disable npm version update checks for this run'),
100
100
  '',
101
101
  paintHeader('Core'),
102
102
  commandRow('revo -i', 'Show runtime info, config path, and auth state'),
103
- commandRow('revo auth login', 'Paste API key and instance ID in the terminal'),
103
+ commandRow('revo auth login', 'Paste API key in the terminal'),
104
104
  commandRow('revo auth status', 'Show the current authentication status'),
105
105
  commandRow('revo auth logout', 'Remove stored credentials'),
106
106
  commandRow('revo project [path]', 'Initialize ambient editor types for JS and TS'),
107
- commandRow('revo project switch <instanceId>', 'Switch the current project to another stored instance'),
108
107
  commandRow('revo endpoints', 'List available API endpoints'),
109
108
  '',
110
109
  paintHeader('Components'),
@@ -153,7 +152,6 @@ export function renderRuntimeInfo(input) {
153
152
  row('Config', configDir),
154
153
  row('Base URL', runtime.baseUrl),
155
154
  row('Status', formatStatusValue(auth)),
156
- row('Instance', runtime.instance || 'missing'),
157
155
  ...(shouldRenderValidationRow(auth) ? [row('Validation', auth.validation || 'skipped')] : []),
158
156
  '',
159
157
  paintHeader('Tips'),
@@ -168,7 +166,6 @@ export function renderAuthStatus(input) {
168
166
  row('Status', formatStatusValue(auth)),
169
167
  row('Config', configDir),
170
168
  row('Base URL', runtime.baseUrl),
171
- row('Instance', runtime.instance || 'missing'),
172
169
  row('API key', runtime.token ? 'configured' : 'missing'),
173
170
  ...(shouldRenderValidationRow(auth) ? [row('Validation', auth.validation || 'skipped')] : []),
174
171
  ].join('\n');
@@ -0,0 +1,16 @@
1
+ import type { ParsedArgs } from './types.ts';
2
+ type UpdateNotifierOptions = {
3
+ args?: ParsedArgs;
4
+ currentVersion: string;
5
+ fetch?: typeof fetch;
6
+ now?: number;
7
+ stream?: NodeJS.WriteStream;
8
+ };
9
+ export declare function compareSemver(left: string, right: string): 0 | 1 | -1;
10
+ export declare function renderUpdateNotice(input: {
11
+ currentVersion: string;
12
+ latestVersion: string;
13
+ stream?: NodeJS.WriteStream;
14
+ }): string;
15
+ export declare function maybeNotifyUpdateAvailable(options: UpdateNotifierOptions): Promise<void>;
16
+ export {};
@@ -0,0 +1,142 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getConfigDir } from "./config.js";
4
+ import { CLI_NAME, CLI_PACKAGE } from "./ui.js";
5
+ import { readJsonFile, writeJsonFile } from "./utils.js";
6
+ const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
7
+ const UPDATE_CHECK_TIMEOUT_MS = 1_000;
8
+ const UPDATE_STATE_FILE = 'update-state.json';
9
+ const NPM_PACKAGE_URL = `https://registry.npmjs.org/${encodeURIComponent(CLI_PACKAGE)}/latest`;
10
+ function isTruthyEnv(value) {
11
+ return value === '1' || value === 'true' || value === 'yes';
12
+ }
13
+ function updateNotifierDisabled(args) {
14
+ return Boolean(args?.['no-update-notifier'])
15
+ || isTruthyEnv(process.env.REVO_NO_UPDATE_NOTIFIER)
16
+ || isTruthyEnv(process.env.NO_UPDATE_NOTIFIER)
17
+ || isTruthyEnv(process.env.CI);
18
+ }
19
+ function getUpdateStatePath() {
20
+ return path.join(getConfigDir(), UPDATE_STATE_FILE);
21
+ }
22
+ function readUpdateState() {
23
+ const filePath = getUpdateStatePath();
24
+ if (!fs.existsSync(filePath)) {
25
+ return null;
26
+ }
27
+ try {
28
+ const raw = readJsonFile(filePath);
29
+ if (raw
30
+ && typeof raw === 'object'
31
+ && !Array.isArray(raw)
32
+ && typeof raw.checkedAt === 'number'
33
+ && typeof raw.latestVersion === 'string') {
34
+ return raw;
35
+ }
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ return null;
41
+ }
42
+ function saveUpdateState(state) {
43
+ try {
44
+ writeJsonFile(getUpdateStatePath(), state);
45
+ }
46
+ catch {
47
+ // Update checks are advisory and must never make a CLI command fail.
48
+ }
49
+ }
50
+ function parseVersion(version) {
51
+ const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
52
+ if (!match) {
53
+ return null;
54
+ }
55
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
56
+ }
57
+ export function compareSemver(left, right) {
58
+ const parsedLeft = parseVersion(left);
59
+ const parsedRight = parseVersion(right);
60
+ if (!parsedLeft || !parsedRight) {
61
+ return 0;
62
+ }
63
+ for (let index = 0; index < parsedLeft.length; index += 1) {
64
+ if (parsedLeft[index] > parsedRight[index]) {
65
+ return 1;
66
+ }
67
+ if (parsedLeft[index] < parsedRight[index]) {
68
+ return -1;
69
+ }
70
+ }
71
+ return 0;
72
+ }
73
+ async function fetchLatestVersion(fetchImpl) {
74
+ const response = await fetchImpl(NPM_PACKAGE_URL, {
75
+ headers: {
76
+ accept: 'application/json',
77
+ },
78
+ signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS),
79
+ });
80
+ if (!response.ok) {
81
+ return '';
82
+ }
83
+ const data = await response.json();
84
+ return data && typeof data === 'object' && typeof data.version === 'string'
85
+ ? data.version
86
+ : '';
87
+ }
88
+ function supportsColor(stream) {
89
+ return Boolean(stream.isTTY || process.env.FORCE_COLOR);
90
+ }
91
+ function color(stream, text, code) {
92
+ return supportsColor(stream) ? `${code}${text}\u001b[0m` : text;
93
+ }
94
+ export function renderUpdateNotice(input) {
95
+ const stream = input.stream || process.stderr;
96
+ const yellow = '\u001b[33m';
97
+ const cyan = '\u001b[36m';
98
+ const bold = '\u001b[1m';
99
+ return [
100
+ color(stream, `Update available ${input.currentVersion} -> ${input.latestVersion}`, bold + yellow),
101
+ `Run ${color(stream, `npm install -g ${CLI_PACKAGE}`, cyan)} to update ${CLI_NAME}.`,
102
+ ].join('\n');
103
+ }
104
+ export async function maybeNotifyUpdateAvailable(options) {
105
+ const stream = options.stream || process.stderr;
106
+ const fetchImpl = options.fetch || globalThis.fetch;
107
+ const now = options.now || Date.now();
108
+ if (updateNotifierDisabled(options.args) || !stream.isTTY || !fetchImpl) {
109
+ return;
110
+ }
111
+ const cached = readUpdateState();
112
+ if (cached && now - cached.checkedAt < UPDATE_CHECK_TTL_MS) {
113
+ if (compareSemver(cached.latestVersion, options.currentVersion) > 0) {
114
+ stream.write(`${renderUpdateNotice({
115
+ currentVersion: options.currentVersion,
116
+ latestVersion: cached.latestVersion,
117
+ stream,
118
+ })}\n`);
119
+ }
120
+ return;
121
+ }
122
+ try {
123
+ const latestVersion = await fetchLatestVersion(fetchImpl);
124
+ if (!latestVersion) {
125
+ return;
126
+ }
127
+ saveUpdateState({
128
+ checkedAt: now,
129
+ latestVersion,
130
+ });
131
+ if (compareSemver(latestVersion, options.currentVersion) > 0) {
132
+ stream.write(`${renderUpdateNotice({
133
+ currentVersion: options.currentVersion,
134
+ latestVersion,
135
+ stream,
136
+ })}\n`);
137
+ }
138
+ }
139
+ catch {
140
+ // Network, registry, and cache failures must not affect the requested command.
141
+ }
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revoengine/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "CLI package for the RevoEngine Platform API",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.build.json",
13
- "check": "node --input-type=module -e \"await import('./src/config.ts'); await import('./src/legacy.ts'); await import('./src/client.ts'); await import('./src/ui.ts'); await import('./src/commands/auth.ts'); await import('./src/commands/component.ts'); await import('./src/commands/endpoints.ts'); await import('./src/commands/info.ts'); await import('./src/commands/request.ts'); await import('./src/commands/search.ts'); await import('./src/cli.ts'); await import('./src/index.ts'); await import('./bin/revo.ts'); await import('./scripts/dev.ts')\"",
13
+ "check": "node --input-type=module -e \"await import('./src/config.ts'); await import('./src/legacy.ts'); await import('./src/client.ts'); await import('./src/ui.ts'); await import('./src/update-notifier.ts'); await import('./src/commands/auth.ts'); await import('./src/commands/component.ts'); await import('./src/commands/endpoints.ts'); await import('./src/commands/info.ts'); await import('./src/commands/request.ts'); await import('./src/commands/search.ts'); await import('./src/cli.ts'); await import('./src/index.ts'); await import('./bin/revo.ts'); await import('./scripts/dev.ts')\"",
14
14
  "dev": "node ./scripts/dev.ts",
15
15
  "prepack": "npm run build",
16
16
  "test": "node --test ./test/*.test.ts"