@sanity/runtime-cli 7.1.0 → 7.3.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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ $ npm install -g @sanity/runtime-cli
20
20
  $ sanity-run COMMAND
21
21
  running command...
22
22
  $ sanity-run (--version)
23
- @sanity/runtime-cli/7.1.0 linux-x64 node-v22.15.0
23
+ @sanity/runtime-cli/7.3.0 linux-x64 node-v22.15.0
24
24
  $ sanity-run --help [COMMAND]
25
25
  USAGE
26
26
  $ sanity-run COMMAND
@@ -84,7 +84,7 @@ EXAMPLES
84
84
  $ sanity-run blueprints add function --name my-function --fn-type document-publish --lang js
85
85
  ```
86
86
 
87
- _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/blueprints/add.ts)_
87
+ _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/blueprints/add.ts)_
88
88
 
89
89
  ## `sanity-run blueprints config`
90
90
 
@@ -112,7 +112,7 @@ EXAMPLES
112
112
  $ sanity-run blueprints config --edit --project-id <projectId>
113
113
  ```
114
114
 
115
- _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/blueprints/config.ts)_
115
+ _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/blueprints/config.ts)_
116
116
 
117
117
  ## `sanity-run blueprints deploy`
118
118
 
@@ -134,7 +134,7 @@ EXAMPLES
134
134
  $ sanity-run blueprints deploy --no-wait
135
135
  ```
136
136
 
137
- _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/blueprints/deploy.ts)_
137
+ _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/blueprints/deploy.ts)_
138
138
 
139
139
  ## `sanity-run blueprints destroy`
140
140
 
@@ -155,7 +155,7 @@ EXAMPLES
155
155
  $ sanity-run blueprints destroy
156
156
  ```
157
157
 
158
- _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/blueprints/destroy.ts)_
158
+ _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/blueprints/destroy.ts)_
159
159
 
160
160
  ## `sanity-run blueprints info`
161
161
 
@@ -172,7 +172,7 @@ EXAMPLES
172
172
  $ sanity-run blueprints info
173
173
  ```
174
174
 
175
- _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/blueprints/info.ts)_
175
+ _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/blueprints/info.ts)_
176
176
 
177
177
  ## `sanity-run blueprints init [DIR]`
178
178
 
@@ -204,7 +204,7 @@ EXAMPLES
204
204
  $ sanity-run blueprints init --blueprint-type <json|js|ts> --project-id <projectId>
205
205
  ```
206
206
 
207
- _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/blueprints/init.ts)_
207
+ _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/blueprints/init.ts)_
208
208
 
209
209
  ## `sanity-run blueprints logs`
210
210
 
@@ -226,7 +226,7 @@ EXAMPLES
226
226
  $ sanity-run blueprints logs --watch
227
227
  ```
228
228
 
229
- _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/blueprints/logs.ts)_
229
+ _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/blueprints/logs.ts)_
230
230
 
231
231
  ## `sanity-run blueprints plan`
232
232
 
@@ -243,7 +243,7 @@ EXAMPLES
243
243
  $ sanity-run blueprints plan
244
244
  ```
245
245
 
246
- _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/blueprints/plan.ts)_
246
+ _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/blueprints/plan.ts)_
247
247
 
248
248
  ## `sanity-run functions dev`
249
249
 
@@ -263,7 +263,7 @@ EXAMPLES
263
263
  $ sanity-run functions dev --port 8974
264
264
  ```
265
265
 
266
- _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/functions/dev.ts)_
266
+ _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/functions/dev.ts)_
267
267
 
268
268
  ## `sanity-run functions env add NAME KEY VALUE`
269
269
 
@@ -285,7 +285,7 @@ EXAMPLES
285
285
  $ sanity-run functions env add MyFunction API_URL https://api.example.com/
286
286
  ```
287
287
 
288
- _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/functions/env/add.ts)_
288
+ _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/functions/env/add.ts)_
289
289
 
290
290
  ## `sanity-run functions env list NAME`
291
291
 
@@ -305,7 +305,7 @@ EXAMPLES
305
305
  $ sanity-run functions env list MyFunction
306
306
  ```
307
307
 
308
- _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/functions/env/list.ts)_
308
+ _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/functions/env/list.ts)_
309
309
 
310
310
  ## `sanity-run functions env remove NAME KEY`
311
311
 
@@ -326,7 +326,7 @@ EXAMPLES
326
326
  $ sanity-run functions env remove MyFunction API_URL
327
327
  ```
328
328
 
329
- _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/functions/env/remove.ts)_
329
+ _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/functions/env/remove.ts)_
330
330
 
331
331
  ## `sanity-run functions logs NAME`
332
332
 
@@ -334,7 +334,7 @@ Retrieve or delete logs for a Sanity Function
334
334
 
335
335
  ```
336
336
  USAGE
337
- $ sanity-run functions logs NAME [-u] [-f [-d | -l <value> | -j]]
337
+ $ sanity-run functions logs NAME [-u] [-f [-d | -l <value> | -j]] [-w]
338
338
 
339
339
  ARGUMENTS
340
340
  NAME The name of the Sanity Function
@@ -345,6 +345,7 @@ FLAGS
345
345
  -j, --json Return logs in JSON format
346
346
  -l, --limit=<value> [default: 50] Total number of log entries to retrieve
347
347
  -u, --utc Show dates in UTC time zone
348
+ -w, --watch Watch for new logs (streaming mode)
348
349
 
349
350
  DESCRIPTION
350
351
  Retrieve or delete logs for a Sanity Function
@@ -359,7 +360,7 @@ EXAMPLES
359
360
  $ sanity-run functions logs <name> --delete
360
361
  ```
361
362
 
362
- _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/functions/logs.ts)_
363
+ _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/functions/logs.ts)_
363
364
 
364
365
  ## `sanity-run functions test NAME`
365
366
 
@@ -392,7 +393,7 @@ EXAMPLES
392
393
  $ sanity-run functions test <name> --data '{ "id": 1 }' --timeout 60
393
394
  ```
394
395
 
395
- _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v7.1.0/src/commands/functions/test.ts)_
396
+ _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v7.3.0/src/commands/functions/test.ts)_
396
397
 
397
398
  ## `sanity-run help [COMMAND]`
398
399
 
@@ -1,4 +1,4 @@
1
- import type { AuthParams } from '../../utils/types.js';
1
+ import type { AuthParams, FunctionLog } from '../../utils/types.js';
2
2
  /** @internal */
3
3
  export interface LoggingOptions {
4
4
  limit: number;
@@ -13,3 +13,4 @@ export declare function deleteLogs(id: string, auth: AuthParams): Promise<{
13
13
  ok: boolean;
14
14
  error: any;
15
15
  }>;
16
+ export declare function streamLogs(id: string, auth: AuthParams, onLog: (log: FunctionLog) => void, onOpen: () => void, onError: (error: string) => void): () => void;
@@ -1,3 +1,4 @@
1
+ import { EventSource } from 'eventsource';
1
2
  import config from '../../config.js';
2
3
  import getHeaders from '../../utils/get-headers.js';
3
4
  const { apiUrl } = config;
@@ -26,3 +27,57 @@ export async function deleteLogs(id, auth) {
26
27
  error: response.ok ? null : json?.error?.message,
27
28
  };
28
29
  }
30
+ // Create streaming logs connection
31
+ export function streamLogs(id, auth, onLog, onOpen, onError) {
32
+ const url = new URL(`${apiUrl}vX/functions/${id}/logs/stream`);
33
+ const headers = getHeaders(auth);
34
+ const eventSource = new EventSource(url.toString(), {
35
+ fetch: (input, init) => fetch(input, {
36
+ ...init,
37
+ headers: {
38
+ ...init?.headers,
39
+ ...headers,
40
+ },
41
+ }),
42
+ });
43
+ eventSource.onopen = onOpen;
44
+ eventSource.onmessage = (event) => {
45
+ try {
46
+ const log = JSON.parse(event.data);
47
+ onLog(log);
48
+ }
49
+ catch (err) {
50
+ onError(`Failed to parse log data: ${err instanceof Error ? err.message : String(err)}`);
51
+ }
52
+ };
53
+ eventSource.addEventListener('logs', (event) => {
54
+ try {
55
+ const logData = JSON.parse(event.data);
56
+ // usually an array
57
+ if (Array.isArray(logData)) {
58
+ for (const log of logData)
59
+ onLog(log);
60
+ }
61
+ else {
62
+ onLog(logData);
63
+ }
64
+ }
65
+ catch (err) {
66
+ console.error('Error parsing logs event:', err);
67
+ }
68
+ });
69
+ eventSource.onerror = (err) => {
70
+ onError('Connection to log stream failed or was closed');
71
+ if (eventSource.readyState === eventSource.CLOSED) {
72
+ console.log('Connection is CLOSED');
73
+ }
74
+ else if (eventSource.readyState === eventSource.CONNECTING) {
75
+ console.log('Connection is attempting to reconnect...');
76
+ return; // Don't close if trying to reconnect
77
+ }
78
+ eventSource.close();
79
+ };
80
+ return () => {
81
+ eventSource.close();
82
+ };
83
+ }
@@ -11,6 +11,7 @@ export default class LogsCommand extends DeployedBlueprintCommand<typeof LogsCom
11
11
  utc: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
13
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ watch: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  };
15
16
  run(): Promise<void>;
16
17
  }
@@ -41,6 +41,11 @@ export default class LogsCommand extends DeployedBlueprintCommand {
41
41
  description: 'Skip confirmation for deleting logs',
42
42
  required: false,
43
43
  }),
44
+ watch: Flags.boolean({
45
+ char: 'w',
46
+ description: 'Watch for new logs (streaming mode)',
47
+ aliases: ['follow'],
48
+ }),
44
49
  };
45
50
  async run() {
46
51
  const { success, error } = await functionLogsCore({
@@ -3,19 +3,22 @@ import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { stashAsset } from '../../actions/blueprints/assets.js';
5
5
  import { getStack, updateStack } from '../../actions/blueprints/stacks.js';
6
+ import { setupLogStreaming } from '../../utils/display/blueprints-logs-streaming.js';
6
7
  import { niceId } from '../../utils/display/presenters.js';
7
8
  import { isLocalFunctionResource } from '../../utils/types.js';
8
9
  export async function blueprintDeployCore(options) {
9
10
  const { bin = 'sanity', log, auth, stackId, projectId, deployedStack, blueprint, flags } = options;
10
11
  const noWait = flags['no-wait'] || false;
12
+ log(`Deploying "${deployedStack.name}" ${niceId(deployedStack.id)}...`);
11
13
  try {
12
14
  const { resources } = blueprint.parsedBlueprint;
13
15
  const validResources = resources?.filter((r) => r.type);
14
16
  const functionResources = validResources?.filter(isLocalFunctionResource);
15
17
  // First stash all function assets
16
18
  if (functionResources?.length) {
19
+ log('Processing function assets...');
17
20
  for (const resource of functionResources) {
18
- const fnSpinner = ora(`Processing ${resource.name}...`).start();
21
+ const fnSpinner = ora({ text: `Processing ${resource.name}...`, prefixText: ' ' }).start();
19
22
  const result = await stashAsset({ resource, auth });
20
23
  if (result.success && result.assetId) {
21
24
  const src = resource.src;
@@ -37,45 +40,59 @@ export async function blueprintDeployCore(options) {
37
40
  };
38
41
  const spinner = ora('Deploying...').start();
39
42
  const { ok: deployOk, stack, error: deployError, } = await updateStack({ stackId, stackPayload, auth });
40
- if (deployOk) {
41
- spinner.succeed(`Deployment "${chalk.bold(stack.name)}" ${niceId(stack.id)} started!`);
42
- if (!noWait) {
43
- const waitSpinner = ora('Waiting for deployment to complete...').start();
44
- while (true) {
45
- // TODO: watch logs and print those while polling
46
- const { ok, stack: currentStack } = await getStack({ stackId: stack.id, auth });
47
- if (!ok) {
48
- waitSpinner.fail('Failed to check deployment status');
49
- return { success: false, error: 'Failed to check deployment status' };
50
- }
51
- const operation = currentStack.recentOperation;
52
- if (!operation) {
53
- waitSpinner.fail('No operation found');
54
- return { success: false, error: 'No operation found' };
55
- }
56
- if (operation.status === 'COMPLETED') {
57
- waitSpinner.succeed('Deployment completed successfully');
58
- break;
59
- }
60
- if (operation.status === 'FAILED') {
61
- waitSpinner.fail('Deployment failed');
62
- return { success: false, error: 'Deployment failed' };
63
- }
64
- await setTimeout(1000);
43
+ if (!deployOk) {
44
+ spinner.fail(`${chalk.red('Failed')} to update deployment`);
45
+ return { success: false, error: deployError || 'Failed to update deployment' };
46
+ }
47
+ spinner.stop().clear();
48
+ if (noWait) {
49
+ log(chalk.bold.green('Deployment started!'));
50
+ log(`Use \`${bin} blueprints info\` to check status`);
51
+ return { success: true };
52
+ }
53
+ log(chalk.dim('Deployment progress:'));
54
+ let logStreamCleanup = null;
55
+ try {
56
+ logStreamCleanup = await setupLogStreaming({
57
+ stackId: stack.id,
58
+ auth,
59
+ log,
60
+ });
61
+ while (true) {
62
+ const { ok, stack: currentStack } = await getStack({ stackId: stack.id, auth });
63
+ if (!ok) {
64
+ if (logStreamCleanup)
65
+ logStreamCleanup();
66
+ return { success: false, error: 'Failed to check deployment status' };
65
67
  }
68
+ const operation = currentStack.recentOperation;
69
+ if (!operation) {
70
+ if (logStreamCleanup)
71
+ logStreamCleanup();
72
+ return { success: false, error: 'No deployment operation found' };
73
+ }
74
+ if (operation.status === 'FAILED') {
75
+ if (logStreamCleanup)
76
+ logStreamCleanup();
77
+ return { success: false, error: 'Deployment failed' };
78
+ }
79
+ if (operation.status === 'COMPLETED') {
80
+ if (logStreamCleanup)
81
+ logStreamCleanup();
82
+ log(chalk.bold.green('Deployment completed!'));
83
+ return { success: true };
84
+ }
85
+ await setTimeout(1500);
66
86
  }
67
- else {
68
- log(`Use \`${bin} blueprints info\` to check status`);
69
- }
70
- return { success: true };
71
87
  }
72
- spinner.fail(`${chalk.red('Failed')} to update deployment`);
73
- log(`Error: ${deployError || JSON.stringify(stack, null, 2) || 'Unknown error'}`);
74
- return { success: false, error: deployError || 'Failed to update deployment' };
88
+ catch (error) {
89
+ if (logStreamCleanup)
90
+ logStreamCleanup();
91
+ throw error;
92
+ }
75
93
  }
76
94
  catch (error) {
77
95
  const errorMessage = error instanceof Error ? error.message : String(error);
78
- log(`Error: ${errorMessage}`);
79
96
  return { success: false, error: errorMessage };
80
97
  }
81
98
  }
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import inquirer from 'inquirer';
4
4
  import ora from 'ora';
5
5
  import { destroyStack, getStack } from '../../actions/blueprints/stacks.js';
6
+ import { setupLogStreaming } from '../../utils/display/blueprints-logs-streaming.js';
6
7
  import { niceId } from '../../utils/display/presenters.js';
7
8
  export async function blueprintDestroyCore(options) {
8
9
  const { bin = 'sanity', log, token, blueprint, flags } = options;
@@ -73,34 +74,47 @@ export async function blueprintDestroyCore(options) {
73
74
  destroySpinner.fail('Failed to destroy deployment');
74
75
  return { success: false, error: error || 'Failed to destroy deployment' };
75
76
  }
77
+ destroySpinner.stop().clear();
76
78
  if (noWait) {
77
- destroySpinner.succeed(`Deployment "${stack.name}" ${niceId(stack.id)} destroy started!`);
79
+ log(chalk.bold.magenta('Destruction started!'));
78
80
  log(`Use \`${bin} blueprints info\` to check status`);
79
81
  return { success: true };
80
82
  }
81
- destroySpinner.stop();
82
- const waitSpinner = ora('Waiting for destruction to complete...').start();
83
- while (true) {
84
- const { ok: pollOk, stack: currentStack } = await getStack({ stackId: stack.id, auth });
85
- const operation = currentStack?.recentOperation;
86
- if (!pollOk || !operation || operation?.status === 'COMPLETED') {
87
- // Operation is also marked destroyed when stack is deleted
88
- // It's possible that the operation is "gone" or available and "COMPLETED"
89
- waitSpinner.succeed(`Deployment "${stack.name}" ${niceId(stack.id)} destroyed`);
90
- break;
83
+ log(chalk.dim('Destruction progress:'));
84
+ let logStreamCleanup = null;
85
+ try {
86
+ logStreamCleanup = await setupLogStreaming({
87
+ stackId: stack.id,
88
+ auth,
89
+ log,
90
+ });
91
+ while (true) {
92
+ const { ok, stack: currentStack } = await getStack({ stackId: stack.id, auth });
93
+ const operation = currentStack?.recentOperation;
94
+ if (!ok || !operation || operation?.status === 'COMPLETED') {
95
+ // Operation is also marked destroyed when stack is deleted;
96
+ // it's possible that the operation is "gone" or available and "COMPLETED"
97
+ if (logStreamCleanup)
98
+ logStreamCleanup();
99
+ log(chalk.bold.magenta('Destruction completed!'));
100
+ return { success: true };
101
+ }
102
+ if (operation.status === 'FAILED') {
103
+ if (logStreamCleanup)
104
+ logStreamCleanup();
105
+ return { success: false, error: 'Destruction failed' };
106
+ }
107
+ await setTimeout(1500);
91
108
  }
92
- if (operation.status === 'FAILED') {
93
- waitSpinner.fail('Destruction failed');
94
- log(`Run \`${bin} blueprints logs\` for more details`);
95
- return { success: false, error: 'Destruction failed' };
96
- }
97
- await setTimeout(1000);
98
109
  }
99
- return { success: true };
110
+ catch (error) {
111
+ if (logStreamCleanup)
112
+ logStreamCleanup();
113
+ throw error;
114
+ }
100
115
  }
101
116
  catch (error) {
102
117
  const errorMessage = error instanceof Error ? error.message : String(error);
103
- log(`Error: ${errorMessage}`);
104
118
  return { success: false, error: errorMessage };
105
119
  }
106
120
  }
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { findNewestLogTimestamp, getLogs, getRecentLogs, isNewerLog, streamLogs, } from '../../actions/blueprints/logs.js';
3
+ import { getLogs, getRecentLogs } from '../../actions/blueprints/logs.js';
4
4
  import { formatTitle } from '../../utils/display/blueprints-formatting.js';
5
+ import { setupLogStreaming } from '../../utils/display/blueprints-logs-streaming.js';
5
6
  import { formatLogEntry, formatLogsByDay, organizeLogsByDay, } from '../../utils/display/logs-formatting.js';
6
7
  import { niceId } from '../../utils/display/presenters.js';
7
8
  export async function blueprintLogsCore(options) {
@@ -28,17 +29,14 @@ export async function blueprintLogsCore(options) {
28
29
  else {
29
30
  log(`No recent logs found for deployment ${niceId(stackId)}`);
30
31
  }
31
- const onOpen = () => {
32
- log(`Watching for new logs... ${chalk.bold('ctrl+c')} to stop`);
33
- };
34
- let newestTimestamp = findNewestLogTimestamp(logs);
35
- const renderLog = (logEntry) => {
36
- if (!isNewerLog(logEntry, newestTimestamp))
37
- return;
38
- newestTimestamp = new Date(logEntry.timestamp).getTime();
39
- log(formatLogEntry(logEntry, true));
40
- };
41
- streamLogs(stackId, auth, renderLog, onOpen, (error) => log(`${chalk.red('Error:')} ${error}`));
32
+ // Set up streaming log display
33
+ const streamCleanup = await setupLogStreaming({
34
+ stackId,
35
+ auth,
36
+ log,
37
+ showBanner: true,
38
+ });
39
+ // Note: don't call streamCleanup() here, it will close the stream
42
40
  // Return a special key for streaming mode
43
41
  return {
44
42
  success: true,
@@ -9,6 +9,7 @@ export interface FunctionLogsOptions extends DeployedBlueprintConfig {
9
9
  utc?: boolean;
10
10
  delete?: boolean;
11
11
  force?: boolean;
12
+ watch?: boolean;
12
13
  };
13
14
  }
14
15
  export declare function functionLogsCore(options: FunctionLogsOptions): Promise<CoreResult>;
@@ -1,16 +1,19 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import ora from 'ora';
4
- import { deleteLogs as deleteLogsAction, logs as getLogsAction, } from '../../actions/functions/logs.js';
4
+ import { deleteLogs as deleteLogsAction, logs as getLogsAction, streamLogs as streamLogsAction, } from '../../actions/functions/logs.js';
5
5
  import { formatTitle } from '../../utils/display/blueprints-formatting.js';
6
+ import { niceId } from '../../utils/display/presenters.js';
6
7
  import { findFunctionByName } from '../../utils/find-function.js';
7
8
  export async function functionLogsCore(options) {
8
9
  const { args, flags, log, auth, deployedStack } = options;
9
10
  const { name } = args;
10
- const { delete: shouldDelete, force, limit, json, utc } = flags;
11
+ const { delete: shouldDelete, watch: shouldWatch, force, limit, json, utc } = flags;
11
12
  const { externalId } = findFunctionByName(deployedStack, name); // throws if not found
12
13
  if (shouldDelete)
13
14
  return deleteLogs({ name, externalId, auth, force, log });
15
+ if (shouldWatch)
16
+ return streamLogs({ name, externalId, auth, json, utc, log });
14
17
  return getLogs({ name, externalId, auth, limit, json, utc, log });
15
18
  }
16
19
  async function deleteLogs({ name, externalId, auth, force, log, }) {
@@ -33,6 +36,34 @@ async function deleteLogs({ name, externalId, auth, force, log, }) {
33
36
  spinner.succeed('Logs deleted');
34
37
  return { success: true };
35
38
  }
39
+ async function streamLogs({ name, externalId, auth, json, utc, log, }) {
40
+ const spinner = ora(`Setting up streaming logs session for function ${niceId(name)}`).start();
41
+ try {
42
+ spinner.stop();
43
+ log(`Streaming log session for function ${niceId(name)}`);
44
+ log(`Watching for new logs... ${chalk.bold('ctrl+c')} to stop`);
45
+ const onOpen = () => {
46
+ // TODO delete line 97 and uncomment 101 when Fns service is updated
47
+ // log(`Watching for new logs... ${chalk.bold('ctrl+c')} to stop`)
48
+ };
49
+ const renderLog = (logEntry) => {
50
+ const { time, level, message } = logEntry;
51
+ log(formatLog(time, level, message, true));
52
+ };
53
+ streamLogsAction(externalId, auth, renderLog, onOpen, (error) => log(`${chalk.red('Error:')} ${error}`));
54
+ // Return a special key for streaming mode
55
+ return {
56
+ success: true,
57
+ streaming: new Promise(() => { }),
58
+ };
59
+ }
60
+ catch (err) {
61
+ spinner.fail('Failed to retrieve logs');
62
+ const errorMessage = err instanceof Error ? err.message : String(err);
63
+ log(`Error: ${errorMessage}`);
64
+ return { success: false, error: errorMessage };
65
+ }
66
+ }
36
67
  async function getLogs({ name, externalId, auth, limit, json, utc, log, }) {
37
68
  const spinner = ora(`Finding logs for function "${name}"`).start();
38
69
  const { ok, error, logs, total } = await getLogsAction(externalId, { limit }, auth);
@@ -53,11 +84,7 @@ async function getLogs({ name, externalId, auth, limit, json, utc, log, }) {
53
84
  }
54
85
  log('\n');
55
86
  for (const { time, level, message } of filteredLogs) {
56
- const date = new Date(time);
57
- const [dateString, timeString] = utc
58
- ? date.toISOString().slice(0, 19).split('T')
59
- : [date.toLocaleDateString(), date.toLocaleTimeString()];
60
- log([chalk.bold(dateString), chalk.bold.blue(timeString), logLevel(level), message].join(' '));
87
+ log(formatLog(time, level, message, utc));
61
88
  }
62
89
  }
63
90
  else {
@@ -65,6 +92,13 @@ async function getLogs({ name, externalId, auth, limit, json, utc, log, }) {
65
92
  }
66
93
  return { success: true };
67
94
  }
95
+ function formatLog(time, level, message, utc) {
96
+ const date = new Date(time);
97
+ const [dateString, timeString] = utc
98
+ ? date.toISOString().slice(0, 19).split('T')
99
+ : [date.toLocaleDateString(), date.toLocaleTimeString()];
100
+ return [chalk.bold(dateString), chalk.bold.blue(timeString), logLevel(level), message].join(' ');
101
+ }
68
102
  function logLevel(level) {
69
103
  if (level === 'ERROR')
70
104
  return chalk.red(level);
@@ -0,0 +1,13 @@
1
+ import type { AuthParams } from '../types.js';
2
+ export interface LogStreamingConfig {
3
+ stackId: string;
4
+ auth: AuthParams;
5
+ showBanner?: boolean;
6
+ log: (message: string) => void;
7
+ }
8
+ /**
9
+ * Sets up log streaming for operations like deploy or destroy with spinner integration
10
+ * @param config Configuration for log streaming
11
+ * @returns A cleanup function for closing the log stream
12
+ */
13
+ export declare function setupLogStreaming(config: LogStreamingConfig): Promise<() => void>;
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk';
2
+ import { isNewerLog, streamLogs } from '../../actions/blueprints/logs.js';
3
+ import { formatLogEntry } from './logs-formatting.js';
4
+ /**
5
+ * Sets up log streaming for operations like deploy or destroy with spinner integration
6
+ * @param config Configuration for log streaming
7
+ * @returns A cleanup function for closing the log stream
8
+ */
9
+ export async function setupLogStreaming(config) {
10
+ const { stackId, auth, log, showBanner } = config;
11
+ let newestTimestamp = new Date().getTime();
12
+ const onLogReceived = (logEntry) => {
13
+ if (!isNewerLog(logEntry, newestTimestamp))
14
+ return;
15
+ newestTimestamp = new Date(logEntry.timestamp).getTime();
16
+ log(formatLogEntry(logEntry, true));
17
+ };
18
+ const onStreamOpen = () => {
19
+ if (showBanner)
20
+ log(`Streaming logs... ${chalk.bold('ctrl+c')} to cancel`);
21
+ };
22
+ const onStreamError = (error) => {
23
+ log(`${chalk.yellow('Stream error:')} ${error}`);
24
+ };
25
+ return streamLogs(stackId, auth, onLogReceived, onStreamOpen, onStreamError);
26
+ }
@@ -618,6 +618,16 @@
618
618
  "required": false,
619
619
  "allowNo": false,
620
620
  "type": "boolean"
621
+ },
622
+ "watch": {
623
+ "aliases": [
624
+ "follow"
625
+ ],
626
+ "char": "w",
627
+ "description": "Watch for new logs (streaming mode)",
628
+ "name": "watch",
629
+ "allowNo": false,
630
+ "type": "boolean"
621
631
  }
622
632
  },
623
633
  "hasDynamicHelp": false,
@@ -829,5 +839,5 @@
829
839
  ]
830
840
  }
831
841
  },
832
- "version": "7.1.0"
842
+ "version": "7.3.0"
833
843
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/runtime-cli",
3
3
  "description": "Sanity's Runtime CLI for Blueprints and Functions",
4
- "version": "7.1.0",
4
+ "version": "7.3.0",
5
5
  "author": "Sanity Runtime Team",
6
6
  "type": "module",
7
7
  "license": "MIT",