netlify-cli 27.14.3 → 27.15.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/dist/commands/logs/index.js +5 -2
- package/dist/commands/logs/log-api.d.ts +1 -1
- package/dist/commands/logs/log-api.js +2 -0
- package/dist/commands/logs/logs.d.ts +1 -1
- package/dist/commands/logs/logs.js +41 -2
- package/dist/commands/logs/sources/server.d.ts +22 -0
- package/dist/commands/logs/sources/server.js +141 -0
- package/dist/utils/deploy/hash-fns.d.ts +1 -0
- package/dist/utils/deploy/hash-fns.js +2 -1
- package/package.json +4 -4
|
@@ -78,14 +78,15 @@ export const createLogsCommand = (program) => {
|
|
|
78
78
|
'functions',
|
|
79
79
|
'edge-functions',
|
|
80
80
|
'deploy',
|
|
81
|
+
'server',
|
|
81
82
|
]))
|
|
82
83
|
.addOption(new Option('--function <name...>', 'Filter to specific functions by name'))
|
|
83
84
|
.addOption(new Option('--edge-function <name...>', 'Filter to specific edge functions by name or path'))
|
|
84
85
|
.addOption(new Option('--since <time>', 'Start of the historical log window. Accepts a duration (e.g. 10m, 1h, 24h) or an ISO 8601 timestamp. Defaults to 10m'))
|
|
85
86
|
.addOption(new Option('--until <time>', 'End of the historical log window. Accepts a duration or an ISO 8601 timestamp (defaults to now)'))
|
|
86
87
|
.addOption(new Option('-f, --follow', 'Stream logs in real time instead of showing historical logs'))
|
|
87
|
-
.addOption(new Option('-u, --url <url>', 'Show logs for the deploy behind the given URL. Supports deploy permalinks and branch subdomains. Can be used with all sources.'))
|
|
88
|
-
.addOption(new Option('-d, --deploy-id <deploy-id>', 'Show logs for a specific deploy by ID, including failed builds. Can be used with all sources.'))
|
|
88
|
+
.addOption(new Option('-u, --url <url>', 'Show logs for the deploy behind the given URL. Supports deploy permalinks and branch subdomains. Can be used with all sources except server.'))
|
|
89
|
+
.addOption(new Option('-d, --deploy-id <deploy-id>', 'Show logs for a specific deploy by ID, including failed builds. Can be used with all sources except server.'))
|
|
89
90
|
.addOption(new Option('-l, --level <levels...>', `Log levels to include. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING}`))
|
|
90
91
|
.addOption(new Option('--json', 'Output logs as JSON Lines'))
|
|
91
92
|
.addExamples([
|
|
@@ -96,6 +97,8 @@ export const createLogsCommand = (program) => {
|
|
|
96
97
|
'netlify logs --source functions --function checkout --since 24h',
|
|
97
98
|
'netlify logs --source edge-functions --since 30m',
|
|
98
99
|
'netlify logs --source deploy --source functions --since 1h',
|
|
100
|
+
'netlify logs --source server --since 30m',
|
|
101
|
+
'netlify logs --follow --source server',
|
|
99
102
|
'netlify logs --follow',
|
|
100
103
|
'netlify logs --follow --source functions --source edge-functions',
|
|
101
104
|
'netlify logs --json --since 1h',
|
|
@@ -4,7 +4,7 @@ export declare const DEPLOY_ID_RE: RegExp;
|
|
|
4
4
|
export declare const SOURCE_INDICATORS: Record<LogEntry['source'], string>;
|
|
5
5
|
export declare const SOURCE_LABELS: Record<LogEntry['source'], string>;
|
|
6
6
|
export interface LogEntry {
|
|
7
|
-
source: 'function' | 'edge-function' | 'deploy';
|
|
7
|
+
source: 'function' | 'edge-function' | 'deploy' | 'server';
|
|
8
8
|
name: string;
|
|
9
9
|
ts: number;
|
|
10
10
|
level: string;
|
|
@@ -7,11 +7,13 @@ export const SOURCE_INDICATORS = {
|
|
|
7
7
|
function: '𝒇',
|
|
8
8
|
'edge-function': '🌐',
|
|
9
9
|
deploy: '🚀',
|
|
10
|
+
server: '🖥️',
|
|
10
11
|
};
|
|
11
12
|
export const SOURCE_LABELS = {
|
|
12
13
|
function: 'Function',
|
|
13
14
|
'edge-function': 'Edge function',
|
|
14
15
|
deploy: 'Deploy',
|
|
16
|
+
server: 'Server',
|
|
15
17
|
};
|
|
16
18
|
const DURATION_LIKE_RE = /^\d+(\.\d+)?\s*[a-z]/i;
|
|
17
19
|
export const parseTimeValue = (input, now = Date.now()) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NetlifyAPI } from '@netlify/api';
|
|
2
2
|
import type BaseCommand from '../base-command.js';
|
|
3
3
|
import type { LogsOptionValues } from './option_values.js';
|
|
4
|
-
type Source = 'functions' | 'edge-functions' | 'deploy';
|
|
4
|
+
type Source = 'functions' | 'edge-functions' | 'deploy' | 'server';
|
|
5
5
|
export declare const logsCommand: (options: LogsOptionValues, command: BaseCommand) => Promise<undefined>;
|
|
6
6
|
export declare const runFollowMode: ({ sources, client, siteId, accessToken, deployId, deployTargeted, functionNames, edgeFunctionNames, levelsToPrint, json, }: {
|
|
7
7
|
sources: Source[];
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { chalk, log, logAndThrowError, netlifyCommand } from '../../utils/command-helpers.js';
|
|
1
|
+
import { chalk, log, logAndThrowError, logError, netlifyCommand, warn } from '../../utils/command-helpers.js';
|
|
2
2
|
import { createColorAssigner, DEPLOY_ID_RE, formatJsonLine, formatLogLine, parseTimeValue, resolveDeployIdFromUrl, SOURCE_INDICATORS, SOURCE_LABELS, } from './log-api.js';
|
|
3
3
|
import { LOG_LEVELS_LIST, CLI_LOG_LEVEL_CHOICES_STRING } from './log-levels.js';
|
|
4
4
|
import { fetchDeployHistoricalLogs, findCurrentBuildingDeploy, findLatestFinishedDeploy, findLatestReadyDeploy, isDeployFinished, streamDeploy, } from './sources/deploy.js';
|
|
5
5
|
import { fetchEdgeFunctionHistoricalLogs, streamEdgeFunctions } from './sources/edge-functions.js';
|
|
6
6
|
import { fetchFunctionHistoricalLogs, listFunctions, selectFunctions, streamFunctions, validateFunctionCount, } from './sources/functions.js';
|
|
7
|
-
|
|
7
|
+
import { fetchServerHistoricalLogs, streamServer } from './sources/server.js';
|
|
8
|
+
const VALID_SOURCES = ['functions', 'edge-functions', 'deploy', 'server'];
|
|
8
9
|
const DEFAULT_SINCE = '10m';
|
|
9
10
|
const DEPLOY_STREAM_IDLE_CLOSE_MS = 3_000;
|
|
10
11
|
const parseSources = (rawSources) => {
|
|
@@ -23,6 +24,7 @@ const SOURCE_TO_ENTRY_SOURCE = {
|
|
|
23
24
|
functions: 'function',
|
|
24
25
|
'edge-functions': 'edge-function',
|
|
25
26
|
deploy: 'deploy',
|
|
27
|
+
server: 'server',
|
|
26
28
|
};
|
|
27
29
|
const humanizeTimeRange = (sinceValue, untilValue) => {
|
|
28
30
|
if (!sinceValue && !untilValue) {
|
|
@@ -45,6 +47,8 @@ const printHeader = (sources, timeDescription, isFollow) => {
|
|
|
45
47
|
return 'edge functions';
|
|
46
48
|
case 'deploy':
|
|
47
49
|
return 'deploy';
|
|
50
|
+
case 'server':
|
|
51
|
+
return 'server';
|
|
48
52
|
}
|
|
49
53
|
});
|
|
50
54
|
const sourceList = sourceNames.length <= 2
|
|
@@ -122,6 +126,9 @@ export const logsCommand = async (options, command) => {
|
|
|
122
126
|
sources = ['functions', 'edge-functions'];
|
|
123
127
|
}
|
|
124
128
|
}
|
|
129
|
+
if (sources.includes('server') && (options.deployId || options.url)) {
|
|
130
|
+
return logAndThrowError('Server logs cannot be filtered by deploy. Remove --deploy-id and --url, or the server source.');
|
|
131
|
+
}
|
|
125
132
|
let deployId;
|
|
126
133
|
let deployTargeted = false;
|
|
127
134
|
if (options.deployId) {
|
|
@@ -265,6 +272,19 @@ const runHistoricalMode = async ({ sources, client, siteId, accessToken, deployI
|
|
|
265
272
|
});
|
|
266
273
|
allEntries.push(...entries);
|
|
267
274
|
}
|
|
275
|
+
let serverLogsTruncated = false;
|
|
276
|
+
if (sources.includes('server')) {
|
|
277
|
+
const { entries, truncated } = await fetchServerHistoricalLogs({
|
|
278
|
+
apiBase: client.basePath,
|
|
279
|
+
siteId,
|
|
280
|
+
accessToken,
|
|
281
|
+
from,
|
|
282
|
+
to,
|
|
283
|
+
levels: levelsToPrint,
|
|
284
|
+
});
|
|
285
|
+
allEntries.push(...entries);
|
|
286
|
+
serverLogsTruncated = truncated;
|
|
287
|
+
}
|
|
268
288
|
allEntries.sort((a, b) => a.ts - b.ts);
|
|
269
289
|
if (allEntries.length === 0) {
|
|
270
290
|
log('No logs found for the given time range.');
|
|
@@ -278,6 +298,15 @@ const runHistoricalMode = async ({ sources, client, siteId, accessToken, deployI
|
|
|
278
298
|
const key = `${entry.source}:${entry.name}`;
|
|
279
299
|
printEntry(entry, levelsToPrint, json, assignColor(key));
|
|
280
300
|
}
|
|
301
|
+
if (serverLogsTruncated) {
|
|
302
|
+
const message = 'Only the most recent server logs are shown. Narrow --since/--until to see older ones.';
|
|
303
|
+
if (json) {
|
|
304
|
+
process.stderr.write(`Warning: ${message}\n`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
warn(message);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
281
310
|
};
|
|
282
311
|
export const runFollowMode = async ({ sources, client, siteId, accessToken, deployId, deployTargeted, functionNames, edgeFunctionNames, levelsToPrint, json, }) => {
|
|
283
312
|
const assignColor = createColorAssigner();
|
|
@@ -316,5 +345,15 @@ export const runFollowMode = async ({ sources, client, siteId, accessToken, depl
|
|
|
316
345
|
if (sources.includes('edge-functions') && deployId) {
|
|
317
346
|
streamEdgeFunctions(siteId, deployId, accessToken, edgeFunctionNames, onEntry);
|
|
318
347
|
}
|
|
348
|
+
if (sources.includes('server')) {
|
|
349
|
+
streamServer({
|
|
350
|
+
apiBase: client.basePath,
|
|
351
|
+
siteId,
|
|
352
|
+
accessToken,
|
|
353
|
+
levels: levelsToPrint,
|
|
354
|
+
onEntry,
|
|
355
|
+
onError: logError,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
319
358
|
};
|
|
320
359
|
//# sourceMappingURL=logs.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LogEntry } from '../log-api.js';
|
|
2
|
+
export declare const fetchServerHistoricalLogs: (options: {
|
|
3
|
+
apiBase: string;
|
|
4
|
+
siteId: string;
|
|
5
|
+
accessToken: string | null | undefined;
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
levels?: string[];
|
|
9
|
+
}) => Promise<{
|
|
10
|
+
entries: LogEntry[];
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const streamServer: ({ apiBase, siteId, accessToken, levels, onEntry, onError, intervalMs, }: {
|
|
14
|
+
apiBase: string;
|
|
15
|
+
siteId: string;
|
|
16
|
+
accessToken: string | null | undefined;
|
|
17
|
+
levels?: string[];
|
|
18
|
+
onEntry: (entry: LogEntry) => void;
|
|
19
|
+
onError?: (error: Error) => void;
|
|
20
|
+
intervalMs?: number;
|
|
21
|
+
}) => (() => void);
|
|
22
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { debugFetch } from '../log-api.js';
|
|
2
|
+
// The API serves up to 500 lines a page and stops paging 10,000 lines back.
|
|
3
|
+
const PER_PAGE = 500;
|
|
4
|
+
const MAX_PAGES = 1 + 10_000 / PER_PAGE;
|
|
5
|
+
// Lines are stored a few seconds after their time, so each poll looks back
|
|
6
|
+
// this far and drops what the previous poll already returned.
|
|
7
|
+
const LOOK_BACK_MS = 10_000;
|
|
8
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
9
|
+
// Retrying cannot change these answers, so following stops on them.
|
|
10
|
+
const PERMANENT_FAILURE_STATUSES = new Set([401, 403, 404]);
|
|
11
|
+
// The API filters by at most one severity, and only these, though lines can
|
|
12
|
+
// carry other levels too.
|
|
13
|
+
const FILTERABLE_SEVERITIES = new Set(['error', 'warn', 'info']);
|
|
14
|
+
const severityFor = (levels) => levels?.length === 1 && FILTERABLE_SEVERITIES.has(levels[0]) ? levels[0] : undefined;
|
|
15
|
+
class ServerLogsRequestError extends Error {
|
|
16
|
+
status;
|
|
17
|
+
constructor(status, statusText) {
|
|
18
|
+
super(`Failed to fetch server logs: ${status.toString()} ${statusText}`);
|
|
19
|
+
this.status = status;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const lineKey = (line) => `${line.timestamp.toString()}|${line.actor_id}|${line.severity}|${line.body}`;
|
|
23
|
+
const countKeys = (lines) => {
|
|
24
|
+
const counts = new Map();
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const key = lineKey(line);
|
|
27
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
28
|
+
}
|
|
29
|
+
return counts;
|
|
30
|
+
};
|
|
31
|
+
const toEntry = (line) => ({
|
|
32
|
+
source: 'server',
|
|
33
|
+
// The dashboard shows instances by the first 8 characters of their ID too.
|
|
34
|
+
name: line.actor_id ? line.actor_id.slice(0, 8) : 'server',
|
|
35
|
+
ts: line.timestamp,
|
|
36
|
+
level: line.severity ? line.severity.toUpperCase() : 'INFO',
|
|
37
|
+
message: line.body,
|
|
38
|
+
});
|
|
39
|
+
const fetchServerLogLines = async ({ apiBase, siteId, accessToken, from, to, levels, }) => {
|
|
40
|
+
// The API pages newest first and sends millisecond timestamps, though it
|
|
41
|
+
// orders by finer ones, so reversing is what keeps same-millisecond lines in order.
|
|
42
|
+
const lines = [];
|
|
43
|
+
const severity = severityFor(levels);
|
|
44
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
45
|
+
const url = new URL(`${apiBase}/sites/${encodeURIComponent(siteId)}/observability/server/logs`);
|
|
46
|
+
url.searchParams.set('from_ts', String(from));
|
|
47
|
+
url.searchParams.set('to_ts', String(to));
|
|
48
|
+
url.searchParams.set('page', String(page));
|
|
49
|
+
url.searchParams.set('per_page', String(PER_PAGE));
|
|
50
|
+
if (severity) {
|
|
51
|
+
url.searchParams.set('severity', severity);
|
|
52
|
+
}
|
|
53
|
+
const response = await debugFetch(url.toString(), {
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${accessToken ?? ''}`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new ServerLogsRequestError(response.status, response.statusText);
|
|
60
|
+
}
|
|
61
|
+
const body = (await response.json());
|
|
62
|
+
lines.push(...(body.logs ?? []));
|
|
63
|
+
if (!body.has_next_page) {
|
|
64
|
+
return { lines: lines.reverse(), truncated: false };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { lines: lines.reverse(), truncated: true };
|
|
68
|
+
};
|
|
69
|
+
export const fetchServerHistoricalLogs = async (options) => {
|
|
70
|
+
const { lines, truncated } = await fetchServerLogLines(options);
|
|
71
|
+
return { entries: lines.map(toEntry), truncated };
|
|
72
|
+
};
|
|
73
|
+
export const streamServer = ({ apiBase, siteId, accessToken, levels, onEntry, onError, intervalMs = POLL_INTERVAL_MS, }) => {
|
|
74
|
+
let newest = Date.now();
|
|
75
|
+
let seen = new Map();
|
|
76
|
+
let failing = false;
|
|
77
|
+
let truncating = false;
|
|
78
|
+
let stopped = false;
|
|
79
|
+
let timer;
|
|
80
|
+
const poll = async () => {
|
|
81
|
+
try {
|
|
82
|
+
const { lines, truncated } = await fetchServerLogLines({
|
|
83
|
+
apiBase,
|
|
84
|
+
siteId,
|
|
85
|
+
accessToken,
|
|
86
|
+
levels,
|
|
87
|
+
from: newest - LOOK_BACK_MS,
|
|
88
|
+
to: Date.now(),
|
|
89
|
+
});
|
|
90
|
+
if (stopped) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Lines carry no ID, so identical ones are told apart by how many of them
|
|
94
|
+
// the previous poll returned.
|
|
95
|
+
const unclaimed = new Map(seen);
|
|
96
|
+
const fresh = lines
|
|
97
|
+
.filter((line) => {
|
|
98
|
+
const key = lineKey(line);
|
|
99
|
+
const count = unclaimed.get(key) ?? 0;
|
|
100
|
+
unclaimed.set(key, count - 1);
|
|
101
|
+
return count <= 0;
|
|
102
|
+
})
|
|
103
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
104
|
+
for (const line of fresh) {
|
|
105
|
+
onEntry(toEntry(line));
|
|
106
|
+
}
|
|
107
|
+
// The API pages newest first, so a truncated window loses its oldest lines for good.
|
|
108
|
+
if (truncated && !truncating) {
|
|
109
|
+
onError?.(new Error('Server logs are arriving faster than they can be shown, so some lines were skipped.'));
|
|
110
|
+
}
|
|
111
|
+
truncating = truncated;
|
|
112
|
+
// The next window starts inside this one, so only these lines can repeat.
|
|
113
|
+
seen = countKeys(lines);
|
|
114
|
+
newest = fresh.reduce((max, line) => Math.max(max, line.timestamp), newest);
|
|
115
|
+
failing = false;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
119
|
+
if (error instanceof ServerLogsRequestError && PERMANENT_FAILURE_STATUSES.has(error.status)) {
|
|
120
|
+
stopped = true;
|
|
121
|
+
onError?.(failure);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// The next poll's window still covers what this one missed, so a run of
|
|
125
|
+
// failures is reported once.
|
|
126
|
+
if (!failing) {
|
|
127
|
+
failing = true;
|
|
128
|
+
onError?.(failure);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!stopped) {
|
|
132
|
+
timer = setTimeout(() => void poll(), intervalMs);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
void poll();
|
|
136
|
+
return () => {
|
|
137
|
+
stopped = true;
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -9,6 +9,7 @@ interface ServerPayload {
|
|
|
9
9
|
routes?: ServerResult['routes'];
|
|
10
10
|
excluded_routes?: ServerResult['excludedRoutes'];
|
|
11
11
|
runtime_version?: ServerResult['runtimeVersion'];
|
|
12
|
+
scale?: ServerResult['scale'];
|
|
12
13
|
}
|
|
13
14
|
type HashedFunctionFile = FunctionUploadFile & {
|
|
14
15
|
hash: string;
|
|
@@ -205,7 +205,7 @@ const hashServer = async (serverBundle, { concurrentHash, hashAlgorithm, statusC
|
|
|
205
205
|
hasherCtor({ concurrentHash, hashAlgorithm }),
|
|
206
206
|
manifestCollectorCtor(servers, serverShaMap, { statusCb }),
|
|
207
207
|
]);
|
|
208
|
-
const { excludedRoutes, instanceSize, region, routes, runtimeVersion } = serverBundle;
|
|
208
|
+
const { excludedRoutes, instanceSize, region, routes, runtimeVersion, scale } = serverBundle;
|
|
209
209
|
const server = {
|
|
210
210
|
sha: Object.values(servers)[0],
|
|
211
211
|
...(region === undefined ? {} : { region }),
|
|
@@ -213,6 +213,7 @@ const hashServer = async (serverBundle, { concurrentHash, hashAlgorithm, statusC
|
|
|
213
213
|
...(routes === undefined ? {} : { routes }),
|
|
214
214
|
...(excludedRoutes === undefined ? {} : { excluded_routes: excludedRoutes }),
|
|
215
215
|
...(runtimeVersion === undefined ? {} : { runtime_version: runtimeVersion }),
|
|
216
|
+
...(scale === undefined ? {} : { scale }),
|
|
216
217
|
};
|
|
217
218
|
return { server, serverShaMap };
|
|
218
219
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "netlify-cli",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.15.0",
|
|
4
4
|
"description": "Netlify command line tool",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"@netlify/ai": "^1.0.2",
|
|
58
58
|
"@netlify/api": "^15.1.3",
|
|
59
59
|
"@netlify/blobs": "^11.1.6",
|
|
60
|
-
"@netlify/build": "^37.
|
|
60
|
+
"@netlify/build": "^37.7.0",
|
|
61
61
|
"@netlify/build-info": "^11.3.0",
|
|
62
|
-
"@netlify/config": "^25.
|
|
62
|
+
"@netlify/config": "^25.5.0",
|
|
63
63
|
"@netlify/dev": "^5.1.10",
|
|
64
64
|
"@netlify/dev-utils": "^6.0.4",
|
|
65
65
|
"@netlify/edge-bundler": "^16.1.3",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@netlify/redirect-matcher": "^0.4.2",
|
|
72
72
|
"@netlify/redirect-parser": "^16.1.1",
|
|
73
73
|
"@netlify/server-dev": "^0.1.4",
|
|
74
|
-
"@netlify/zip-it-and-ship-it": "^16.
|
|
74
|
+
"@netlify/zip-it-and-ship-it": "^16.6.0",
|
|
75
75
|
"@opentelemetry/api": "~1.9.0",
|
|
76
76
|
"@pnpm/tabtab": "^0.5.4",
|
|
77
77
|
"ansi-escapes": "^7.3.0",
|