@pmate/cli 0.19.0 → 0.20.1
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/index.js +182 -91
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,24 +34,94 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
34
34
|
));
|
|
35
35
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
36
36
|
|
|
37
|
+
// src/namespacedRef.ts
|
|
38
|
+
function parseNamespacedRef(value, options = {}) {
|
|
39
|
+
const raw = value.trim();
|
|
40
|
+
const entityLabel = options.entityLabel || "ref";
|
|
41
|
+
if (!raw) {
|
|
42
|
+
throw new Error(`Invalid ${entityLabel}.`);
|
|
43
|
+
}
|
|
44
|
+
const colonIndex = raw.indexOf(":");
|
|
45
|
+
if (colonIndex > 0) {
|
|
46
|
+
const namespace2 = raw.slice(0, colonIndex).trim();
|
|
47
|
+
const objectId = raw.slice(colonIndex + 1).trim();
|
|
48
|
+
if (!namespace2 || !objectId) {
|
|
49
|
+
throw new Error(`Invalid ${entityLabel} "${value}". Use <ns:objectId>.`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
namespace: namespace2,
|
|
53
|
+
objectId,
|
|
54
|
+
fullId: `${namespace2}:${objectId}`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const namespace = options.defaultNamespace?.trim();
|
|
58
|
+
if (namespace) {
|
|
59
|
+
return {
|
|
60
|
+
namespace,
|
|
61
|
+
objectId: raw,
|
|
62
|
+
fullId: `${namespace}:${raw}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Invalid ${entityLabel} "${value}". Use <ns:objectId>.`);
|
|
66
|
+
}
|
|
67
|
+
function composeNamespacedRef(namespace, objectId) {
|
|
68
|
+
const normalizedNamespace = namespace.trim();
|
|
69
|
+
const normalizedObjectId = objectId.trim();
|
|
70
|
+
return `${normalizedNamespace}:${normalizedObjectId}`;
|
|
71
|
+
}
|
|
72
|
+
function formatNamespacedDisplayId(namespace, objectIdOrFullId) {
|
|
73
|
+
const raw = objectIdOrFullId.trim();
|
|
74
|
+
if (raw.includes(":")) {
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
77
|
+
const ns = namespace?.trim();
|
|
78
|
+
if (!ns) {
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
return composeNamespacedRef(ns, raw);
|
|
82
|
+
}
|
|
83
|
+
var init_namespacedRef = __esm({
|
|
84
|
+
"src/namespacedRef.ts"() {
|
|
85
|
+
"use strict";
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
37
89
|
// src/sessionStore.ts
|
|
38
90
|
function buildSessionStorePath() {
|
|
39
91
|
return import_node_path3.default.join(import_node_os2.default.homedir(), ".pmate", "session.json");
|
|
40
92
|
}
|
|
93
|
+
function assertValidAppRef(appRef) {
|
|
94
|
+
return parseNamespacedRef(appRef, { entityLabel: "app ref" }).fullId;
|
|
95
|
+
}
|
|
41
96
|
function saveLoginSession(session, requestedApp = DEFAULT_APP_ID) {
|
|
42
97
|
const store = loadExistingSessionStoreOrEmpty();
|
|
43
|
-
const app =
|
|
98
|
+
const app = assertValidAppRef(requestedApp);
|
|
99
|
+
const sessionApp = session.session?.identity?.app?.trim();
|
|
100
|
+
if (!sessionApp) {
|
|
101
|
+
throw new Error("Missing app in login session payload.");
|
|
102
|
+
}
|
|
103
|
+
const normalizedSessionApp = assertValidAppRef(sessionApp);
|
|
104
|
+
if (normalizedSessionApp !== app) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Session app mismatch. requested=${app}, received=${normalizedSessionApp}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const accountId = session.session.identity?.accountId?.trim();
|
|
110
|
+
const token = session.token?.trim();
|
|
111
|
+
const issuedAt = session.session.issuedAt?.trim();
|
|
112
|
+
const expiresAt = session.session.expiresAt?.trim();
|
|
113
|
+
if (!accountId || !token || !issuedAt || !expiresAt) {
|
|
114
|
+
throw new Error("Invalid login session payload.");
|
|
115
|
+
}
|
|
44
116
|
const nextSession = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...session.session.app ? {} : { app }
|
|
50
|
-
},
|
|
117
|
+
token,
|
|
118
|
+
accountId,
|
|
119
|
+
issuedAt,
|
|
120
|
+
expiresAt,
|
|
51
121
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
52
122
|
};
|
|
53
123
|
store.sessions[app] = nextSession;
|
|
54
|
-
store.
|
|
124
|
+
store.activeAppRef = app;
|
|
55
125
|
writeSessionStore(store);
|
|
56
126
|
return {
|
|
57
127
|
app,
|
|
@@ -67,7 +137,9 @@ function readRequiredSession(options) {
|
|
|
67
137
|
}
|
|
68
138
|
const token = session.token?.trim();
|
|
69
139
|
if (!token) {
|
|
70
|
-
throw new Error(
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Missing token in ${sessionPath}. Run "pmate login --app ${resolvedApp}" again.`
|
|
142
|
+
);
|
|
71
143
|
}
|
|
72
144
|
return {
|
|
73
145
|
...session,
|
|
@@ -82,7 +154,7 @@ function loadExistingSessionStoreOrEmpty() {
|
|
|
82
154
|
if (!(0, import_node_fs2.existsSync)(sessionPath)) {
|
|
83
155
|
return {
|
|
84
156
|
version: SESSION_STORE_VERSION,
|
|
85
|
-
|
|
157
|
+
activeAppRef: DEFAULT_APP_ID,
|
|
86
158
|
sessions: {}
|
|
87
159
|
};
|
|
88
160
|
}
|
|
@@ -104,31 +176,74 @@ function normalizeStore(value) {
|
|
|
104
176
|
`Invalid session store format in ${buildSessionStorePath()}. Run "pmate login" again.`
|
|
105
177
|
);
|
|
106
178
|
}
|
|
107
|
-
return
|
|
108
|
-
version: SESSION_STORE_VERSION,
|
|
109
|
-
activeApp: value.activeApp || DEFAULT_APP_ID,
|
|
110
|
-
sessions: value.sessions || {}
|
|
111
|
-
};
|
|
179
|
+
return value;
|
|
112
180
|
}
|
|
113
181
|
function isSessionStore(value) {
|
|
114
|
-
|
|
182
|
+
if (!value || typeof value !== "object") {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const candidate = value;
|
|
186
|
+
if (candidate.version !== SESSION_STORE_VERSION) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (typeof candidate.activeAppRef !== "string") {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
let normalizedActiveApp;
|
|
193
|
+
try {
|
|
194
|
+
normalizedActiveApp = assertValidAppRef(candidate.activeAppRef);
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (normalizedActiveApp !== candidate.activeAppRef) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
if (!candidate.sessions || typeof candidate.sessions !== "object") {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (!(normalizedActiveApp in candidate.sessions)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
for (const [appRef, session] of Object.entries(candidate.sessions)) {
|
|
208
|
+
try {
|
|
209
|
+
if (assertValidAppRef(appRef) !== appRef) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (!isStoredSession(session)) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
115
220
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
221
|
+
function isStoredSession(value) {
|
|
222
|
+
if (!value || typeof value !== "object") {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const session = value;
|
|
226
|
+
return typeof session.token === "string" && !!session.token.trim() && typeof session.accountId === "string" && !!session.accountId.trim() && typeof session.issuedAt === "string" && !!session.issuedAt.trim() && typeof session.expiresAt === "string" && !!session.expiresAt.trim() && typeof session.updatedAt === "string" && !!session.updatedAt.trim();
|
|
118
227
|
}
|
|
119
228
|
function resolveReadApp(store, options) {
|
|
120
229
|
const requestedApp = options?.app?.trim();
|
|
121
230
|
if (requestedApp) {
|
|
122
|
-
return requestedApp;
|
|
231
|
+
return assertValidAppRef(requestedApp);
|
|
123
232
|
}
|
|
124
233
|
const preferredApp = options?.preferredApp?.trim();
|
|
125
|
-
if (preferredApp
|
|
126
|
-
|
|
234
|
+
if (preferredApp) {
|
|
235
|
+
const normalizedPreferred = assertValidAppRef(preferredApp);
|
|
236
|
+
if (store.sessions[normalizedPreferred]) {
|
|
237
|
+
return normalizedPreferred;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (options?.fallbackToActiveApp && store.sessions[store.activeAppRef]) {
|
|
241
|
+
return store.activeAppRef;
|
|
127
242
|
}
|
|
128
|
-
if (
|
|
129
|
-
return
|
|
243
|
+
if (preferredApp) {
|
|
244
|
+
return assertValidAppRef(preferredApp);
|
|
130
245
|
}
|
|
131
|
-
return
|
|
246
|
+
return store.activeAppRef;
|
|
132
247
|
}
|
|
133
248
|
function writeSessionStore(store) {
|
|
134
249
|
const sessionPath = buildSessionStorePath();
|
|
@@ -154,8 +269,9 @@ var init_sessionStore = __esm({
|
|
|
154
269
|
import_node_fs2 = require("fs");
|
|
155
270
|
import_node_os2 = __toESM(require("os"));
|
|
156
271
|
import_node_path3 = __toESM(require("path"));
|
|
157
|
-
|
|
158
|
-
|
|
272
|
+
init_namespacedRef();
|
|
273
|
+
DEFAULT_APP_ID = "pmate:cli";
|
|
274
|
+
SESSION_STORE_VERSION = 3;
|
|
159
275
|
}
|
|
160
276
|
});
|
|
161
277
|
|
|
@@ -13025,23 +13141,24 @@ var ListCommandHandler = class {
|
|
|
13025
13141
|
};
|
|
13026
13142
|
|
|
13027
13143
|
// src/commands/LoginCommandHandler.ts
|
|
13028
|
-
var import_node_http = require("http");
|
|
13029
13144
|
var import_node_child_process = require("child_process");
|
|
13145
|
+
var import_node_http = require("http");
|
|
13030
13146
|
init_sessionStore();
|
|
13031
13147
|
var AUTH_UI_BASE_URL = "https://auth.pmate.chat";
|
|
13032
13148
|
var AUTH_API_BASE_URL = "https://auth-api-v2.pmate.chat";
|
|
13033
13149
|
var CALLBACK_PATH = "/pmate-login-callback";
|
|
13034
13150
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
13035
13151
|
function buildHostedLoginUrl(redirectUrl, appId = DEFAULT_APP_ID) {
|
|
13152
|
+
const resolvedApp = assertValidAppRef(appId);
|
|
13036
13153
|
const url = new URL(AUTH_UI_BASE_URL);
|
|
13037
13154
|
url.searchParams.set("redirect", redirectUrl);
|
|
13038
|
-
url.searchParams.set("app",
|
|
13155
|
+
url.searchParams.set("app", resolvedApp);
|
|
13039
13156
|
return url.toString();
|
|
13040
13157
|
}
|
|
13041
13158
|
var LoginCommandHandler = class {
|
|
13042
13159
|
async handle(args) {
|
|
13043
13160
|
const callback = await this.startCallbackServer();
|
|
13044
|
-
const appId = args.app?.trim() || DEFAULT_APP_ID;
|
|
13161
|
+
const appId = assertValidAppRef(args.app?.trim() || DEFAULT_APP_ID);
|
|
13045
13162
|
const loginUrl = buildHostedLoginUrl(callback.redirectUrl, appId);
|
|
13046
13163
|
let savedSession = null;
|
|
13047
13164
|
ui.info("Opening browser for login.", {
|
|
@@ -13058,7 +13175,7 @@ var LoginCommandHandler = class {
|
|
|
13058
13175
|
});
|
|
13059
13176
|
try {
|
|
13060
13177
|
const token = await callback.waitForToken;
|
|
13061
|
-
const loginResponse = await this.
|
|
13178
|
+
const loginResponse = await this.exchangeSession(token, appId);
|
|
13062
13179
|
savedSession = saveLoginSession(loginResponse, appId);
|
|
13063
13180
|
} finally {
|
|
13064
13181
|
await callback.close();
|
|
@@ -13069,26 +13186,27 @@ var LoginCommandHandler = class {
|
|
|
13069
13186
|
);
|
|
13070
13187
|
}
|
|
13071
13188
|
}
|
|
13072
|
-
async
|
|
13073
|
-
const
|
|
13074
|
-
|
|
13075
|
-
const response = await fetch(url.toString(), {
|
|
13076
|
-
method: "GET",
|
|
13189
|
+
async exchangeSession(token, appId) {
|
|
13190
|
+
const response = await fetch(`${AUTH_API_BASE_URL}/session/exchange`, {
|
|
13191
|
+
method: "POST",
|
|
13077
13192
|
headers: {
|
|
13193
|
+
"Content-Type": "application/json",
|
|
13078
13194
|
Authorization: `Bearer ${token}`
|
|
13079
|
-
}
|
|
13195
|
+
},
|
|
13196
|
+
body: JSON.stringify({ app: appId })
|
|
13080
13197
|
});
|
|
13081
13198
|
if (!response.ok) {
|
|
13082
13199
|
const detail = await response.text();
|
|
13083
13200
|
throw new Error(
|
|
13084
|
-
`Failed to
|
|
13201
|
+
`Failed to exchange app session (${response.status}): ${detail || response.statusText}`
|
|
13085
13202
|
);
|
|
13086
13203
|
}
|
|
13087
13204
|
const resp = await response.json();
|
|
13088
|
-
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
}
|
|
13205
|
+
const returnedApp = resp?.data?.session?.identity?.app?.trim();
|
|
13206
|
+
if (!returnedApp || returnedApp !== appId) {
|
|
13207
|
+
throw new Error("Invalid exchange response: app-scoped session mismatch.");
|
|
13208
|
+
}
|
|
13209
|
+
return resp.data;
|
|
13092
13210
|
}
|
|
13093
13211
|
async startCallbackServer() {
|
|
13094
13212
|
let resolveToken;
|
|
@@ -13097,9 +13215,16 @@ var LoginCommandHandler = class {
|
|
|
13097
13215
|
resolveToken = resolve;
|
|
13098
13216
|
rejectToken = reject;
|
|
13099
13217
|
});
|
|
13218
|
+
const openSockets = /* @__PURE__ */ new Set();
|
|
13100
13219
|
const server = (0, import_node_http.createServer)((request, response) => {
|
|
13101
13220
|
this.handleCallbackRequest(request, response, resolveToken, rejectToken);
|
|
13102
13221
|
});
|
|
13222
|
+
server.on("connection", (socket) => {
|
|
13223
|
+
openSockets.add(socket);
|
|
13224
|
+
socket.on("close", () => {
|
|
13225
|
+
openSockets.delete(socket);
|
|
13226
|
+
});
|
|
13227
|
+
});
|
|
13103
13228
|
await new Promise((resolve, reject) => {
|
|
13104
13229
|
server.once("error", reject);
|
|
13105
13230
|
server.listen(0, "127.0.0.1", () => resolve());
|
|
@@ -13119,9 +13244,15 @@ var LoginCommandHandler = class {
|
|
|
13119
13244
|
}, LOGIN_TIMEOUT_MS);
|
|
13120
13245
|
const close = async () => {
|
|
13121
13246
|
clearTimeout(timeout);
|
|
13122
|
-
|
|
13247
|
+
const closePromise = new Promise((resolve) => {
|
|
13123
13248
|
server.close(() => resolve());
|
|
13124
13249
|
});
|
|
13250
|
+
server.closeIdleConnections?.();
|
|
13251
|
+
server.closeAllConnections?.();
|
|
13252
|
+
for (const socket of openSockets) {
|
|
13253
|
+
socket.destroy();
|
|
13254
|
+
}
|
|
13255
|
+
await closePromise;
|
|
13125
13256
|
};
|
|
13126
13257
|
return {
|
|
13127
13258
|
redirectUrl,
|
|
@@ -13146,13 +13277,14 @@ var LoginCommandHandler = class {
|
|
|
13146
13277
|
}
|
|
13147
13278
|
response.statusCode = 200;
|
|
13148
13279
|
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
13280
|
+
response.setHeader("Connection", "close");
|
|
13149
13281
|
response.end(
|
|
13150
13282
|
this.renderHtml(
|
|
13151
13283
|
"Login complete",
|
|
13152
13284
|
"You can close this tab and return to the terminal."
|
|
13153
|
-
)
|
|
13285
|
+
),
|
|
13286
|
+
() => resolveToken(token)
|
|
13154
13287
|
);
|
|
13155
|
-
resolveToken(token);
|
|
13156
13288
|
}
|
|
13157
13289
|
renderHtml(title, message) {
|
|
13158
13290
|
return `<!doctype html>
|
|
@@ -18990,54 +19122,8 @@ var NamespaceRegistry = class {
|
|
|
18990
19122
|
}
|
|
18991
19123
|
};
|
|
18992
19124
|
|
|
18993
|
-
// src/namespacedRef.ts
|
|
18994
|
-
function parseNamespacedRef(value, options = {}) {
|
|
18995
|
-
const raw = value.trim();
|
|
18996
|
-
const entityLabel = options.entityLabel || "ref";
|
|
18997
|
-
if (!raw) {
|
|
18998
|
-
throw new Error(`Invalid ${entityLabel}.`);
|
|
18999
|
-
}
|
|
19000
|
-
const colonIndex = raw.indexOf(":");
|
|
19001
|
-
if (colonIndex > 0) {
|
|
19002
|
-
const namespace2 = raw.slice(0, colonIndex).trim();
|
|
19003
|
-
const objectId = raw.slice(colonIndex + 1).trim();
|
|
19004
|
-
if (!namespace2 || !objectId) {
|
|
19005
|
-
throw new Error(`Invalid ${entityLabel} "${value}". Use <ns:objectId>.`);
|
|
19006
|
-
}
|
|
19007
|
-
return {
|
|
19008
|
-
namespace: namespace2,
|
|
19009
|
-
objectId,
|
|
19010
|
-
fullId: `${namespace2}:${objectId}`
|
|
19011
|
-
};
|
|
19012
|
-
}
|
|
19013
|
-
const namespace = options.defaultNamespace?.trim();
|
|
19014
|
-
if (namespace) {
|
|
19015
|
-
return {
|
|
19016
|
-
namespace,
|
|
19017
|
-
objectId: raw,
|
|
19018
|
-
fullId: `${namespace}:${raw}`
|
|
19019
|
-
};
|
|
19020
|
-
}
|
|
19021
|
-
throw new Error(`Invalid ${entityLabel} "${value}". Use <ns:objectId>.`);
|
|
19022
|
-
}
|
|
19023
|
-
function composeNamespacedRef(namespace, objectId) {
|
|
19024
|
-
const normalizedNamespace = namespace.trim();
|
|
19025
|
-
const normalizedObjectId = objectId.trim();
|
|
19026
|
-
return `${normalizedNamespace}:${normalizedObjectId}`;
|
|
19027
|
-
}
|
|
19028
|
-
function formatNamespacedDisplayId(namespace, objectIdOrFullId) {
|
|
19029
|
-
const raw = objectIdOrFullId.trim();
|
|
19030
|
-
if (raw.includes(":")) {
|
|
19031
|
-
return raw;
|
|
19032
|
-
}
|
|
19033
|
-
const ns = namespace?.trim();
|
|
19034
|
-
if (!ns) {
|
|
19035
|
-
return raw;
|
|
19036
|
-
}
|
|
19037
|
-
return composeNamespacedRef(ns, raw);
|
|
19038
|
-
}
|
|
19039
|
-
|
|
19040
19125
|
// src/commands/AgentCommandHandler.ts
|
|
19126
|
+
init_namespacedRef();
|
|
19041
19127
|
var AgentCommandHandler = class {
|
|
19042
19128
|
async handle(args) {
|
|
19043
19129
|
const token = this.readToken();
|
|
@@ -19501,6 +19587,7 @@ var StoreCommandHandler = class {
|
|
|
19501
19587
|
var import_node_fs13 = require("fs");
|
|
19502
19588
|
var import_node_path18 = __toESM(require("path"));
|
|
19503
19589
|
init_sessionStore();
|
|
19590
|
+
init_namespacedRef();
|
|
19504
19591
|
var AppCommandHandler = class {
|
|
19505
19592
|
async handle(args) {
|
|
19506
19593
|
const token = this.readToken();
|
|
@@ -20208,6 +20295,7 @@ function normalizeRepository(repository) {
|
|
|
20208
20295
|
}
|
|
20209
20296
|
|
|
20210
20297
|
// src/commands/ServiceCommandHandler.ts
|
|
20298
|
+
init_namespacedRef();
|
|
20211
20299
|
var ServiceCommandHandler = class {
|
|
20212
20300
|
async handle(args) {
|
|
20213
20301
|
const baseUrl = this.resolveBaseUrl(args.baseUrl);
|
|
@@ -20723,6 +20811,7 @@ var AccountCommandHandler = class {
|
|
|
20723
20811
|
};
|
|
20724
20812
|
|
|
20725
20813
|
// src/agentCommandParser.ts
|
|
20814
|
+
init_namespacedRef();
|
|
20726
20815
|
var DEFAULT_AGENT_NAMESPACE = "pmate";
|
|
20727
20816
|
function parseAgentArgs(tokens) {
|
|
20728
20817
|
const rest = [...tokens];
|
|
@@ -21231,6 +21320,7 @@ function parsePage(value) {
|
|
|
21231
21320
|
}
|
|
21232
21321
|
|
|
21233
21322
|
// src/appCommandParser.ts
|
|
21323
|
+
init_namespacedRef();
|
|
21234
21324
|
function parseAppArgs(tokens) {
|
|
21235
21325
|
const rest = [...tokens];
|
|
21236
21326
|
const rawAction = rest.shift();
|
|
@@ -21420,6 +21510,7 @@ function parseBareRenameId2(value, entityName) {
|
|
|
21420
21510
|
}
|
|
21421
21511
|
|
|
21422
21512
|
// src/serviceCommandParser.ts
|
|
21513
|
+
init_namespacedRef();
|
|
21423
21514
|
function parseServiceArgs(tokens) {
|
|
21424
21515
|
const rest = [...tokens];
|
|
21425
21516
|
const rawAction = rest.shift();
|