@revoengine/cli 1.0.1 → 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
@@ -29,6 +29,8 @@ revo auth login
29
29
 
30
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
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.
33
+
32
34
  Check the active session:
33
35
 
34
36
  ```bash
@@ -134,11 +136,13 @@ Components with `category: null` are stored under `Components/__no_category__/..
134
136
  Bulk sync behavior:
135
137
 
136
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.
137
140
  - Pull compares the full local workspace contract before overwriting anything.
138
141
  - Pull skips with `no changes` when the local workspace already matches the remote component.
139
142
  - Pull skips with `changed` when local files differ from the remote contract.
140
143
  - Pull skips with `stale version` when the local version is older than the remote version, unless `--stale` or `--force` is passed.
141
144
  - Push treats backend `Not modified` responses as skipped instead of failing the whole run.
145
+ - Push treats backend `404` responses as skipped with `doesn't exist remotely`; restore the component in RevoEngine before pushing local changes to it.
142
146
  - Debug posts the local `component.json` plus `elements/{order}_{key}.{js|ts}` source files to the authenticated sandbox `debug` endpoint.
143
147
  - Sync logs show direction explicitly: `RevoEngine -> path` for pull and `RevoEngine <- path` for push.
144
148
  - Bulk runs print a summary such as `Deployed 54/67, Skipped 13/67 in 13s`.
package/dist/src/cli.js CHANGED
@@ -138,6 +138,7 @@ function printError(error) {
138
138
  function resolveClient(args) {
139
139
  const runtime = resolveRuntimeConfig({
140
140
  baseUrl: typeof args.url === 'string' ? args.url : typeof args.baseUrl === 'string' ? args.baseUrl : undefined,
141
+ instance: typeof args.instance === 'string' ? args.instance : typeof args.i === 'string' ? args.i : undefined,
141
142
  token: typeof args.token === 'string' ? args.token : typeof args.t === 'string' ? args.t : undefined,
142
143
  });
143
144
  return new RevoClient(runtime);
@@ -11,6 +11,7 @@ export type ComponentListRequest = {
11
11
  };
12
12
  export type ClientOptions = {
13
13
  baseUrl?: string;
14
+ instance?: string;
14
15
  token?: string;
15
16
  fetch?: typeof fetch;
16
17
  };
@@ -34,8 +35,10 @@ export declare class PermissionDeniedError extends ApiError {
34
35
  }
35
36
  declare function buildUrl(baseUrl: string, requestPath: string, query?: Record<string, unknown>): URL;
36
37
  declare function normalizeToken(token?: string): string;
38
+ export declare function extractProfileInstanceId(profile: unknown): string;
37
39
  export declare class RevoClient {
38
40
  baseUrl: string;
41
+ instance: string;
39
42
  token: string;
40
43
  fetchImpl: typeof fetch | undefined;
41
44
  constructor(options?: ClientOptions);
@@ -64,7 +67,6 @@ export declare class RevoClient {
64
67
  search(params: Record<string, unknown>): Promise<unknown>;
65
68
  listComponents(options?: ComponentListRequest): Promise<unknown>;
66
69
  getComponent(componentId: string): Promise<unknown>;
67
- createComponent(body: Record<string, unknown>): Promise<ApiResponse<unknown>>;
68
70
  saveComponentElements(componentId: string, body: unknown): Promise<ApiResponse<unknown>>;
69
71
  }
70
72
  export { buildUrl, normalizeToken };
@@ -125,13 +125,39 @@ 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;
154
+ instance;
130
155
  token;
131
156
  fetchImpl;
132
157
  constructor(options = {}) {
133
158
  const config = resolveRuntimeConfig(options);
134
159
  this.baseUrl = config.baseUrl;
160
+ this.instance = config.instance || '';
135
161
  this.token = config.token;
136
162
  this.fetchImpl = options.fetch || globalThis.fetch;
137
163
  }
@@ -141,6 +167,7 @@ export class RevoClient {
141
167
  get authValidationKey() {
142
168
  return buildAuthValidationKey({
143
169
  baseUrl: this.baseUrl,
170
+ instance: this.instance,
144
171
  token: this.token,
145
172
  });
146
173
  }
@@ -164,6 +191,7 @@ export class RevoClient {
164
191
  throw new AuthenticationError(401, 'Not authenticated. Run `revo auth login`.', null);
165
192
  }
166
193
  if (cached.profile !== undefined) {
194
+ this.instance = this.instance || extractProfileInstanceId(cached.profile);
167
195
  return {
168
196
  authenticated: true,
169
197
  fromCache: true,
@@ -173,6 +201,7 @@ export class RevoClient {
173
201
  }
174
202
  try {
175
203
  const profile = await this.requestData('GET', '/api/v1/me', { authGuard: false });
204
+ this.instance = this.instance || extractProfileInstanceId(profile);
176
205
  saveAuthValidationState({
177
206
  key: this.authValidationKey,
178
207
  status: 'authenticated',
@@ -206,6 +235,10 @@ export class RevoClient {
206
235
  const headers = new Headers(options.headers || {});
207
236
  headers.set('Authorization', this.authHeader);
208
237
  headers.set('x-api-key', this.token);
238
+ if (this.instance) {
239
+ headers.set('instance', this.instance);
240
+ headers.set('x-api-instance', this.instance);
241
+ }
209
242
  let body = options.body;
210
243
  if (body !== undefined && body !== null && method.toUpperCase() !== 'GET') {
211
244
  headers.set('Content-Type', 'application/json');
@@ -294,9 +327,6 @@ export class RevoClient {
294
327
  async getComponent(componentId) {
295
328
  return this.requestData('GET', `/api/v1/component/${componentId}`);
296
329
  }
297
- async createComponent(body) {
298
- return this.request('POST', '/api/v1/component', { body });
299
- }
300
330
  async saveComponentElements(componentId, body) {
301
331
  return this.request('POST', `/api/v1/component/${componentId}/save`, { body });
302
332
  }
@@ -62,6 +62,7 @@ export async function handleAuthCommand(context) {
62
62
  println(`Imported legacy config from ${filePath}.`);
63
63
  if (!offline) {
64
64
  client.token = imported.token;
65
+ client.instance = imported.instance;
65
66
  client.baseUrl = imported.baseUrl;
66
67
  if (imported.token) {
67
68
  try {
@@ -99,7 +100,10 @@ export async function handleAuthCommand(context) {
99
100
  error(`Saved credentials, but validation failed: ${String(validationError)}`);
100
101
  return;
101
102
  }
102
- saveStoredConfig(next);
103
+ saveStoredConfig({
104
+ ...next,
105
+ instance: client.instance || undefined,
106
+ });
103
107
  clearAuthValidationState();
104
108
  println(`Logged in and saved credentials to ${getConfigDir()}.`);
105
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
  }
@@ -2,13 +2,15 @@ export declare const APP_NAME = "revoengine";
2
2
  export declare const DEFAULT_BASE_URL = "https://api.revoengine.com";
3
3
  export type RuntimeConfig = {
4
4
  baseUrl: string;
5
+ instance: string;
5
6
  token: string;
6
7
  };
7
8
  export type RuntimeConfigOptions = {
8
9
  baseUrl?: string;
10
+ instance?: string;
9
11
  token?: string;
10
12
  };
11
- type LegacyRuntimeConfig = RuntimeConfig & {
13
+ type StoredConfig = Omit<RuntimeConfig, 'instance'> & {
12
14
  instance?: string;
13
15
  };
14
16
  export type AuthValidationState = {
@@ -27,7 +29,7 @@ export declare function getConfigPaths(): {
27
29
  };
28
30
  export declare function readProjectInstanceId(startDir?: string): string;
29
31
  export declare function isUuid(value: unknown): value is string;
30
- export declare function saveStoredConfig(nextConfig: Partial<LegacyRuntimeConfig>): void;
32
+ export declare function saveStoredConfig(nextConfig: Partial<StoredConfig>): void;
31
33
  export declare function clearStoredConfig(): void;
32
34
  export declare function loadStoredConfigIndex(): {
33
35
  defaultInstance: string;
@@ -42,13 +44,14 @@ export declare function loadStoredConfigIndex(): {
42
44
  };
43
45
  export declare function loadStoredConfig(options?: {
44
46
  allowLegacy?: boolean;
45
- }): RuntimeConfig;
47
+ }): StoredConfig;
46
48
  export declare function buildAuthValidationKey(runtime: RuntimeConfigOptions): string;
47
49
  export declare function readAuthValidationState(key?: string): AuthValidationState;
48
50
  export declare function saveAuthValidationState(state: AuthValidationState): void;
49
51
  export declare function clearAuthValidationState(key?: string): void;
50
52
  export declare function resolveRuntimeConfig(options?: RuntimeConfigOptions): {
51
53
  baseUrl: string;
54
+ instance: string;
52
55
  token: string;
53
56
  };
54
57
  export {};
@@ -73,10 +73,13 @@ export function saveStoredConfig(nextConfig) {
73
73
  const { dir, configFile, credentialsFile } = getConfigPaths();
74
74
  ensureDirectory(dir);
75
75
  const current = readStoredConfigFile();
76
- writeJsonFile(configFile, {
76
+ const instance = nextConfig.instance || current.instance || '';
77
+ const config = {
77
78
  baseUrl: nextConfig.baseUrl || current.baseUrl || DEFAULT_BASE_URL,
78
79
  token: nextConfig.token || current.token || '',
79
- });
80
+ ...(instance ? { instance } : {}),
81
+ };
82
+ writeJsonFile(configFile, config);
80
83
  removeFileIfExists(credentialsFile);
81
84
  }
82
85
  export function clearStoredConfig() {
@@ -156,6 +159,7 @@ function parseFlatConfig(raw, credentials) {
156
159
  : typeof raw.url === 'string'
157
160
  ? raw.url
158
161
  : DEFAULT_BASE_URL,
162
+ ...(typeof raw.instance === 'string' && isUuid(raw.instance) ? { instance: raw.instance } : {}),
159
163
  token,
160
164
  };
161
165
  }
@@ -181,6 +185,7 @@ function parseMappedConfig(raw) {
181
185
  return selected
182
186
  ? {
183
187
  baseUrl: selected.baseUrl,
188
+ instance: defaultInstance,
184
189
  token: selected.token,
185
190
  }
186
191
  : emptyStoredConfigFile();
@@ -194,6 +199,9 @@ function parseLegacyConfig(config, credentials) {
194
199
  const token = isRecord(credentials) && typeof credentials.token === 'string' ? credentials.token : '';
195
200
  return {
196
201
  baseUrl,
202
+ ...(isRecord(config) && typeof config.instance === 'string' && isUuid(config.instance)
203
+ ? { instance: config.instance }
204
+ : {}),
197
205
  token,
198
206
  };
199
207
  }
@@ -263,6 +271,7 @@ export function buildAuthValidationKey(runtime) {
263
271
  return createHash('sha256')
264
272
  .update(JSON.stringify({
265
273
  baseUrl: runtime.baseUrl || DEFAULT_BASE_URL,
274
+ instance: runtime.instance || '',
266
275
  token: runtime.token || '',
267
276
  }))
268
277
  .digest('hex');
@@ -346,11 +355,13 @@ export function clearAuthValidationState(key) {
346
355
  export function resolveRuntimeConfig(options = {}) {
347
356
  const env = {
348
357
  baseUrl: resolveEnvValue(['REVO_URL', 'REVO_BASE_URL', 'REVOENGINE_URL', 'REVOENGINE_BASE_URL']),
358
+ instance: resolveEnvValue(['REVO_INSTANCE', 'REVOENGINE_INSTANCE']),
349
359
  token: resolveEnvValue(['REVO_TOKEN', 'REVO_API_KEY', 'REVOENGINE_TOKEN', 'REVOENGINE_API_KEY']),
350
360
  };
351
361
  const stored = loadStoredConfig();
352
362
  return {
353
363
  baseUrl: options.baseUrl || env.baseUrl || stored.baseUrl || DEFAULT_BASE_URL,
364
+ instance: options.instance || env.instance || stored.instance || '',
354
365
  token: options.token || env.token || stored.token || '',
355
366
  };
356
367
  }
@@ -7,6 +7,11 @@ function resolveArgsRuntime(context) {
7
7
  : typeof context.args.baseUrl === 'string'
8
8
  ? context.args.baseUrl
9
9
  : undefined,
10
+ instance: typeof context.args.instance === 'string'
11
+ ? context.args.instance
12
+ : typeof context.args.i === 'string'
13
+ ? context.args.i
14
+ : undefined,
10
15
  token: typeof context.args.token === 'string'
11
16
  ? context.args.token
12
17
  : typeof context.args.t === 'string'
@@ -46,6 +51,7 @@ export async function buildRuntimeViewModel(context, options = {}) {
46
51
  };
47
52
  if (runtime.token) {
48
53
  context.client.baseUrl = runtime.baseUrl;
54
+ context.client.instance = runtime.instance || '';
49
55
  context.client.token = runtime.token;
50
56
  try {
51
57
  const profile = await context.client.me({ force: options.forceValidation });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revoengine/cli",
3
- "version": "1.0.1",
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",