@agentage/cli 0.38.1 → 0.40.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 +111 -25
- package/dist/commands/auth/setup.d.ts +8 -2
- package/dist/commands/auth/setup.js +38 -7
- package/dist/commands/extension/account.d.ts +5 -0
- package/dist/commands/extension/account.js +18 -0
- package/dist/commands/extension/check.d.ts +10 -0
- package/dist/commands/extension/check.js +35 -0
- package/dist/commands/extension/delegate.d.ts +4 -0
- package/dist/commands/extension/delegate.js +45 -0
- package/dist/commands/extension/install-load.d.ts +7 -0
- package/dist/commands/extension/install-load.js +33 -0
- package/dist/commands/extension/install.d.ts +10 -0
- package/dist/commands/extension/install.js +35 -0
- package/dist/commands/extension/latest.d.ts +2 -0
- package/dist/commands/extension/latest.js +70 -0
- package/dist/commands/extension/list.d.ts +17 -0
- package/dist/commands/extension/list.js +102 -0
- package/dist/commands/extension/plan.d.ts +8 -0
- package/dist/commands/extension/plan.js +67 -0
- package/dist/commands/extension/remove.d.ts +15 -0
- package/dist/commands/extension/remove.js +86 -0
- package/dist/commands/extension/row-lines.d.ts +4 -0
- package/dist/commands/extension/row-lines.js +68 -0
- package/dist/commands/extension/rows.d.ts +56 -0
- package/dist/commands/extension/rows.js +61 -0
- package/dist/commands/extension/secrets-prompt.d.ts +6 -0
- package/dist/commands/extension/secrets-prompt.js +47 -0
- package/dist/commands/extension/setup-cloud.d.ts +6 -0
- package/dist/commands/extension/setup-cloud.js +98 -0
- package/dist/commands/extension/setup.d.ts +5 -0
- package/dist/commands/extension/setup.js +68 -0
- package/dist/commands/extension/sync.d.ts +22 -0
- package/dist/commands/extension/sync.js +81 -0
- package/dist/commands/status/account-status.d.ts +3 -0
- package/dist/commands/status/account-status.js +27 -0
- package/dist/commands/status/status.d.ts +7 -1
- package/dist/commands/status/status.js +50 -6
- package/dist/core/installer/compare-lock.d.ts +8 -0
- package/dist/core/installer/compare-lock.js +51 -0
- package/dist/core/installer/compat.d.ts +12 -0
- package/dist/core/installer/compat.js +35 -0
- package/dist/core/installer/installer.d.ts +28 -0
- package/dist/core/installer/installer.js +87 -0
- package/dist/core/installer/latest.d.ts +26 -0
- package/dist/core/installer/latest.js +88 -0
- package/dist/core/installer/npm.d.ts +11 -0
- package/dist/core/installer/npm.js +76 -0
- package/dist/core/installer/plan.d.ts +25 -0
- package/dist/core/installer/plan.js +52 -0
- package/dist/core/installer/resolve.d.ts +25 -0
- package/dist/core/installer/resolve.js +139 -0
- package/dist/core/installer/tree.d.ts +21 -0
- package/dist/core/installer/tree.js +100 -0
- package/dist/core/loader/config.d.ts +7 -0
- package/dist/core/loader/config.js +59 -0
- package/dist/core/loader/ctx.d.ts +16 -0
- package/dist/core/loader/ctx.js +76 -0
- package/dist/core/loader/dev-path.d.ts +1 -0
- package/dist/core/loader/dev-path.js +20 -0
- package/dist/core/loader/discover.d.ts +35 -0
- package/dist/core/loader/discover.js +118 -0
- package/dist/core/loader/dispose.d.ts +4 -0
- package/dist/core/loader/dispose.js +24 -0
- package/dist/core/loader/load.d.ts +13 -0
- package/dist/core/loader/load.js +61 -0
- package/dist/core/loader/log.d.ts +3 -0
- package/dist/core/loader/log.js +23 -0
- package/dist/core/loader/manifest.d.ts +14 -0
- package/dist/core/loader/manifest.js +77 -0
- package/dist/core/loader/order.d.ts +2 -0
- package/dist/core/loader/order.js +20 -0
- package/dist/core/loader/register.d.ts +19 -0
- package/dist/core/loader/register.js +39 -0
- package/dist/core/loader/scan.d.ts +18 -0
- package/dist/core/loader/scan.js +34 -0
- package/dist/core/loader/secrets.d.ts +9 -0
- package/dist/core/loader/secrets.js +28 -0
- package/dist/core/loader/state.d.ts +12 -0
- package/dist/core/loader/state.js +31 -0
- package/dist/core/loader/states.d.ts +7 -0
- package/dist/core/loader/states.js +52 -0
- package/dist/core/loader/timeout.d.ts +1 -0
- package/dist/core/loader/timeout.js +8 -0
- package/dist/core/loader/use.d.ts +8 -0
- package/dist/core/loader/use.js +18 -0
- package/dist/core/paths.d.ts +8 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/profile/cloud.d.ts +53 -0
- package/dist/core/profile/cloud.js +109 -0
- package/dist/core/profile/edit.d.ts +4 -0
- package/dist/core/profile/edit.js +27 -0
- package/dist/core/profile/machine.d.ts +18 -0
- package/dist/core/profile/machine.js +83 -0
- package/dist/core/profile/merge.d.ts +11 -0
- package/dist/core/profile/merge.js +21 -0
- package/dist/core/profile/profile.d.ts +6 -0
- package/dist/core/profile/profile.js +68 -0
- package/dist/core/profile/push.d.ts +16 -0
- package/dist/core/profile/push.js +49 -0
- package/dist/core/profile/register.d.ts +5 -0
- package/dist/core/profile/register.js +57 -0
- package/dist/core/profile/schema.d.ts +28 -0
- package/dist/core/profile/schema.js +165 -0
- package/dist/core/semver-range.d.ts +17 -0
- package/dist/core/semver-range.js +124 -0
- package/dist/extension-test.d.ts +7 -0
- package/dist/extension-test.js +49 -0
- package/dist/extension.d.ts +83 -0
- package/dist/extension.js +6 -0
- package/dist/lib/auth/api.d.ts +2 -2
- package/dist/lib/auth/api.js +3 -3
- package/dist/lib/auth/callback-page.d.ts +2 -0
- package/dist/lib/auth/callback-page.js +40 -0
- package/dist/lib/auth/callback-server.js +1 -4
- package/dist/lib/fs/file-lock.d.ts +3 -1
- package/dist/lib/fs/file-lock.js +26 -11
- package/dist/lib/fs/json-file.d.ts +1 -0
- package/dist/lib/fs/json-file.js +3 -2
- package/dist/lib/net/fetch.js +1 -1
- package/dist/lib/net/origins.d.ts +1 -0
- package/dist/lib/net/origins.js +11 -11
- package/dist/lib/npm/npm-cli.d.ts +2 -0
- package/dist/lib/npm/npm-cli.js +4 -4
- package/dist/program.d.ts +9 -1
- package/dist/program.js +44 -10
- package/package.json +8 -4
- package/dist/core/flag.d.ts +0 -1
- package/dist/core/flag.js +0 -1
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.js +0 -2
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { VERSION } from '../../utils/version.js';
|
|
2
|
+
import { errorCode } from './cloud.js';
|
|
3
|
+
import {} from './machine.js';
|
|
4
|
+
// Fire and forget: never longer than this, never a failure, silent unless AGENTAGE_DEBUG=1.
|
|
5
|
+
const BUDGET_MS = 2_000;
|
|
6
|
+
const NAME_MAX = 128;
|
|
7
|
+
const routeOf = (id) => `/v1/machines/${id}`;
|
|
8
|
+
const debug = (text) => {
|
|
9
|
+
if (process.env['AGENTAGE_DEBUG'] === '1')
|
|
10
|
+
console.error(`machines: ${text}`);
|
|
11
|
+
};
|
|
12
|
+
// A 404's body is read for its code; any other body is dropped unread.
|
|
13
|
+
const answerOf = async (res) => {
|
|
14
|
+
if (res.status === 404)
|
|
15
|
+
return { status: 404, code: await errorCode(res) };
|
|
16
|
+
await res.body?.cancel();
|
|
17
|
+
return { status: res.status, code: null };
|
|
18
|
+
};
|
|
19
|
+
// What the account answered, or null when it did not.
|
|
20
|
+
const send = async (fetch, call, timeoutMs = BUDGET_MS) => {
|
|
21
|
+
const headers = call.body ? { 'content-type': 'application/json' } : {};
|
|
22
|
+
const body = call.body ? JSON.stringify(call.body) : undefined;
|
|
23
|
+
try {
|
|
24
|
+
const answer = await answerOf(await fetch(call.path, { method: call.method, headers, body, timeoutMs }));
|
|
25
|
+
debug(`${call.method} ${call.path} ${answer.status}${answer.code ? ` ${answer.code}` : ''}`);
|
|
26
|
+
return answer;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const reason = err.name === 'TimeoutError' ? 'timed out' : err.message;
|
|
30
|
+
debug(`${call.method} ${call.path} failed: ${reason}`);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const registration = (machine) => ({
|
|
35
|
+
name: machine.name.slice(0, NAME_MAX),
|
|
36
|
+
platform: `${process.platform}-${process.arch}`,
|
|
37
|
+
cliVersion: VERSION,
|
|
38
|
+
state: 'connected',
|
|
39
|
+
});
|
|
40
|
+
// Signed-in setup: create or refresh the record, then mark it seen, both inside one budget.
|
|
41
|
+
export const registerMachine = async (fetch, machine) => {
|
|
42
|
+
const deadline = Date.now() + BUDGET_MS;
|
|
43
|
+
const path = routeOf(machine.id);
|
|
44
|
+
await send(fetch, { method: 'PUT', path, body: registration(machine) });
|
|
45
|
+
await send(fetch, { method: 'POST', path: `${path}/seen` }, Math.max(1, deadline - Date.now()));
|
|
46
|
+
};
|
|
47
|
+
// true when the account knows this machine, false on MACHINE_NOT_FOUND, null when it did not
|
|
48
|
+
// say: any other 404 is an API without the route, as unknown as no answer.
|
|
49
|
+
export const seenMachine = async (fetch, id) => {
|
|
50
|
+
const answer = await send(fetch, { method: 'POST', path: `${routeOf(id)}/seen` });
|
|
51
|
+
if (answer?.code === 'MACHINE_NOT_FOUND')
|
|
52
|
+
return false;
|
|
53
|
+
return answer !== null && answer.status >= 200 && answer.status < 300 ? true : null;
|
|
54
|
+
};
|
|
55
|
+
export const disconnectMachine = async (fetch, id) => {
|
|
56
|
+
await send(fetch, { method: 'PATCH', path: routeOf(id), body: { state: 'disconnected' } });
|
|
57
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const SCHEMA_VERSION = 1;
|
|
2
|
+
export type ConfigMap = Record<string, Record<string, unknown>>;
|
|
3
|
+
export interface MachineEntry {
|
|
4
|
+
off: string[];
|
|
5
|
+
config: ConfigMap;
|
|
6
|
+
extra: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface Profile {
|
|
9
|
+
version: number;
|
|
10
|
+
extensions: Record<string, string>;
|
|
11
|
+
registries: Record<string, string>;
|
|
12
|
+
config: ConfigMap;
|
|
13
|
+
machines: Record<string, MachineEntry>;
|
|
14
|
+
extra: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export type ProfileParse = {
|
|
17
|
+
ok: true;
|
|
18
|
+
profile: Profile;
|
|
19
|
+
readOnly: boolean;
|
|
20
|
+
warnings: string[];
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
errors: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare const PACKAGE_NAME: RegExp;
|
|
26
|
+
export declare const specProblem: (spec: string) => string | null;
|
|
27
|
+
export declare const parseProfile: (doc: unknown) => ProfileParse;
|
|
28
|
+
export declare const toDocument: (profile: Profile) => Record<string, unknown>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isRecord } from '../../lib/fs/json-file.js';
|
|
2
|
+
import { isRange } from '../semver-range.js';
|
|
3
|
+
export const SCHEMA_VERSION = 1;
|
|
4
|
+
const TOP_KEYS = ['version', 'extensions', 'registries', 'config', 'machines'];
|
|
5
|
+
const MACHINE_KEYS = ['off', 'config'];
|
|
6
|
+
export const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
7
|
+
const SCOPE = /^@[a-z0-9][a-z0-9._~-]*$/;
|
|
8
|
+
const LOOPBACK_HOST = /^(?:localhost|.+\.localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[::1?\])$/i;
|
|
9
|
+
const REFUSED_PREFIX = /^(npm|github|file|http):/i;
|
|
10
|
+
const OTHER_PROTOCOL = /^[a-z][a-z0-9+.-]*:/i;
|
|
11
|
+
const LOCAL_PATH = /^(?:\.{1,2}[/\\]|~?[/\\])/;
|
|
12
|
+
const EMPTY_MACHINE = { off: [], config: {}, extra: {} };
|
|
13
|
+
const USE_INSTEAD = 'use a semver range or an https: tarball URL';
|
|
14
|
+
const at = (name) => `[${JSON.stringify(name)}]`;
|
|
15
|
+
const fail = (issues, message, fallback) => {
|
|
16
|
+
issues.errors.push(message);
|
|
17
|
+
return fallback;
|
|
18
|
+
};
|
|
19
|
+
const urlOf = (text) => {
|
|
20
|
+
try {
|
|
21
|
+
return new URL(text);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const httpsProblem = (spec) => {
|
|
28
|
+
const url = urlOf(spec);
|
|
29
|
+
if (url === null)
|
|
30
|
+
return 'is not a valid https: URL';
|
|
31
|
+
return LOOPBACK_HOST.test(url.hostname) ? 'loopback URLs are refused' : null;
|
|
32
|
+
};
|
|
33
|
+
export const specProblem = (spec) => {
|
|
34
|
+
const refused = REFUSED_PREFIX.exec(spec)?.[1];
|
|
35
|
+
if (refused)
|
|
36
|
+
return `${refused.toLowerCase()}: specs are refused; ${USE_INSTEAD}`;
|
|
37
|
+
if (/^https:/i.test(spec))
|
|
38
|
+
return httpsProblem(spec);
|
|
39
|
+
if (LOCAL_PATH.test(spec))
|
|
40
|
+
return 'local paths are dev-only and belong in machine.json';
|
|
41
|
+
if (OTHER_PROTOCOL.test(spec))
|
|
42
|
+
return `unsupported spec; ${USE_INSTEAD}`;
|
|
43
|
+
return isRange(spec) ? null : `is not a semver range; ${USE_INSTEAD}`;
|
|
44
|
+
};
|
|
45
|
+
const extensionProblem = (name, spec) => {
|
|
46
|
+
if (!PACKAGE_NAME.test(name))
|
|
47
|
+
return 'is not a valid package name';
|
|
48
|
+
if (typeof spec !== 'string' || spec.trim() === '')
|
|
49
|
+
return 'must be a non-empty string';
|
|
50
|
+
return specProblem(spec.trim());
|
|
51
|
+
};
|
|
52
|
+
// Whitespace is refused outright: the URL lands on one line of the tree's .npmrc.
|
|
53
|
+
const registryProblem = (scope, url) => {
|
|
54
|
+
if (!SCOPE.test(scope))
|
|
55
|
+
return 'is not an npm scope (@name)';
|
|
56
|
+
const parsed = typeof url === 'string' && !/\s/.test(url) ? urlOf(url) : null;
|
|
57
|
+
if (parsed && (parsed.username || parsed.password))
|
|
58
|
+
return 'must not carry credentials';
|
|
59
|
+
if (parsed?.protocol === 'https:')
|
|
60
|
+
return null;
|
|
61
|
+
if (parsed?.protocol === 'http:' && LOOPBACK_HOST.test(parsed.hostname))
|
|
62
|
+
return null;
|
|
63
|
+
return 'must be an https: URL, or http: on a loopback host';
|
|
64
|
+
};
|
|
65
|
+
// A map of strings: each entry is checked on its own and reported by its path.
|
|
66
|
+
const parseMap = (value, path, problemOf, issues) => {
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return {};
|
|
69
|
+
if (!isRecord(value))
|
|
70
|
+
return fail(issues, `${path}: must be an object`, {});
|
|
71
|
+
const map = {};
|
|
72
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
73
|
+
const problem = problemOf(key, entry);
|
|
74
|
+
if (problem)
|
|
75
|
+
issues.errors.push(`${path}${at(key)}: ${problem}`);
|
|
76
|
+
else
|
|
77
|
+
map[key] = String(entry);
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
80
|
+
};
|
|
81
|
+
const parseConfig = (value, path, issues) => {
|
|
82
|
+
if (value === undefined)
|
|
83
|
+
return {};
|
|
84
|
+
if (!isRecord(value))
|
|
85
|
+
return fail(issues, `${path}: must be an object`, {});
|
|
86
|
+
const config = {};
|
|
87
|
+
for (const [id, slice] of Object.entries(value)) {
|
|
88
|
+
if (isRecord(slice))
|
|
89
|
+
config[id] = slice;
|
|
90
|
+
else
|
|
91
|
+
issues.errors.push(`${path}${at(id)}: must be an object`);
|
|
92
|
+
}
|
|
93
|
+
return config;
|
|
94
|
+
};
|
|
95
|
+
const extraKeys = (doc, known, path, issues) => {
|
|
96
|
+
const extra = Object.fromEntries(Object.entries(doc).filter(([key]) => !known.includes(key)));
|
|
97
|
+
for (const key of Object.keys(extra))
|
|
98
|
+
issues.warnings.push(`${path}: unknown key "${key}" kept`);
|
|
99
|
+
return extra;
|
|
100
|
+
};
|
|
101
|
+
const parseOff = (value, path, issues) => {
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return [];
|
|
104
|
+
if (Array.isArray(value) && value.every((name) => typeof name === 'string'))
|
|
105
|
+
return value;
|
|
106
|
+
return fail(issues, `${path}.off: must be an array of package names`, []);
|
|
107
|
+
};
|
|
108
|
+
const parseMachine = (value, path, issues) => {
|
|
109
|
+
if (!isRecord(value))
|
|
110
|
+
return fail(issues, `${path}: must be an object`, EMPTY_MACHINE);
|
|
111
|
+
return {
|
|
112
|
+
off: parseOff(value['off'], path, issues),
|
|
113
|
+
config: parseConfig(value['config'], `${path}.config`, issues),
|
|
114
|
+
extra: extraKeys(value, MACHINE_KEYS, path, issues),
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const parseMachines = (value, issues) => {
|
|
118
|
+
if (value === undefined)
|
|
119
|
+
return {};
|
|
120
|
+
if (!isRecord(value))
|
|
121
|
+
return fail(issues, 'machines: must be an object', {});
|
|
122
|
+
return Object.fromEntries(Object.entries(value).map(([id, entry]) => [
|
|
123
|
+
id,
|
|
124
|
+
parseMachine(entry, `machines${at(id)}`, issues),
|
|
125
|
+
]));
|
|
126
|
+
};
|
|
127
|
+
const parseVersionField = (value, issues) => {
|
|
128
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 1)
|
|
129
|
+
return value;
|
|
130
|
+
const got = value === undefined ? 'missing' : JSON.stringify(value);
|
|
131
|
+
issues.errors.push(`version: must be ${SCHEMA_VERSION} (got ${got})`);
|
|
132
|
+
return SCHEMA_VERSION;
|
|
133
|
+
};
|
|
134
|
+
const readOnlyWarning = (version) => `version: ${version} is newer than this CLI understands (${SCHEMA_VERSION}); read-only`;
|
|
135
|
+
export const parseProfile = (doc) => {
|
|
136
|
+
if (!isRecord(doc))
|
|
137
|
+
return { ok: false, errors: ['profile.json: must be a JSON object'] };
|
|
138
|
+
const issues = { errors: [], warnings: [] };
|
|
139
|
+
const profile = {
|
|
140
|
+
version: parseVersionField(doc['version'], issues),
|
|
141
|
+
extensions: parseMap(doc['extensions'], 'extensions', extensionProblem, issues),
|
|
142
|
+
registries: parseMap(doc['registries'], 'registries', registryProblem, issues),
|
|
143
|
+
config: parseConfig(doc['config'], 'config', issues),
|
|
144
|
+
machines: parseMachines(doc['machines'], issues),
|
|
145
|
+
extra: extraKeys(doc, TOP_KEYS, 'profile.json', issues),
|
|
146
|
+
};
|
|
147
|
+
if (issues.errors.length > 0)
|
|
148
|
+
return { ok: false, errors: issues.errors };
|
|
149
|
+
const readOnly = profile.version > SCHEMA_VERSION;
|
|
150
|
+
if (readOnly)
|
|
151
|
+
issues.warnings.push(readOnlyWarning(profile.version));
|
|
152
|
+
return { ok: true, profile, readOnly, warnings: issues.warnings };
|
|
153
|
+
};
|
|
154
|
+
// The inverse of parseProfile: unknown keys go back where they were found.
|
|
155
|
+
export const toDocument = (profile) => ({
|
|
156
|
+
version: profile.version,
|
|
157
|
+
extensions: profile.extensions,
|
|
158
|
+
...(Object.keys(profile.registries).length > 0 ? { registries: profile.registries } : {}),
|
|
159
|
+
config: profile.config,
|
|
160
|
+
machines: Object.fromEntries(Object.entries(profile.machines).map(([id, m]) => [
|
|
161
|
+
id,
|
|
162
|
+
{ off: m.off, config: m.config, ...m.extra },
|
|
163
|
+
])),
|
|
164
|
+
...profile.extra,
|
|
165
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface Version {
|
|
2
|
+
major: number;
|
|
3
|
+
minor: number;
|
|
4
|
+
patch: number;
|
|
5
|
+
pre: string | null;
|
|
6
|
+
}
|
|
7
|
+
type Op = '<' | '<=' | '>' | '>=' | '=';
|
|
8
|
+
interface Comparator {
|
|
9
|
+
op: Op;
|
|
10
|
+
v: Version;
|
|
11
|
+
}
|
|
12
|
+
export declare const parseVersion: (text: string) => Version | null;
|
|
13
|
+
export declare const parseRange: (range: string) => Comparator[][] | null;
|
|
14
|
+
export declare const isRange: (range: string) => boolean;
|
|
15
|
+
export declare const satisfies: (version: string, range: string) => boolean;
|
|
16
|
+
export declare const newestFirst: (a: string, b: string) => number;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const WILD = /^[xX*]$/;
|
|
2
|
+
const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3
|
+
const PARTIAL_RE = /^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
4
|
+
const COMPARATOR_RE = /^(\^|~|>=|<=|>|<|=)?(.+)$/;
|
|
5
|
+
const ver = (major, minor, patch, pre = null) => ({
|
|
6
|
+
major,
|
|
7
|
+
minor,
|
|
8
|
+
patch,
|
|
9
|
+
pre,
|
|
10
|
+
});
|
|
11
|
+
export const parseVersion = (text) => {
|
|
12
|
+
const m = VERSION_RE.exec(text.trim());
|
|
13
|
+
if (!m)
|
|
14
|
+
return null;
|
|
15
|
+
return ver(Number(m[1]), Number(m[2]), Number(m[3]), m[4] ?? null);
|
|
16
|
+
};
|
|
17
|
+
const parsePartial = (text) => {
|
|
18
|
+
const m = PARTIAL_RE.exec(text);
|
|
19
|
+
if (!m)
|
|
20
|
+
return null;
|
|
21
|
+
const raw = [m[1], m[2], m[3]];
|
|
22
|
+
const nums = [];
|
|
23
|
+
for (const part of raw) {
|
|
24
|
+
if (part === undefined || WILD.test(part))
|
|
25
|
+
break;
|
|
26
|
+
nums.push(Number(part));
|
|
27
|
+
}
|
|
28
|
+
const concreteAfterWild = raw.slice(nums.length).some((p) => p !== undefined && !WILD.test(p));
|
|
29
|
+
if (concreteAfterWild || (m[4] !== undefined && nums.length < 3))
|
|
30
|
+
return null;
|
|
31
|
+
return { nums, pre: m[4] ?? null };
|
|
32
|
+
};
|
|
33
|
+
const lower = (p) => ver(p.nums[0] ?? 0, p.nums[1] ?? 0, p.nums[2] ?? 0, p.pre);
|
|
34
|
+
// Exclusive upper bound one step above part `i`; the `-0` keeps that version's prereleases out.
|
|
35
|
+
const bump = (p, i) => {
|
|
36
|
+
const [a = 0, b = 0, c = 0] = p.nums;
|
|
37
|
+
if (i <= 0)
|
|
38
|
+
return ver(a + 1, 0, 0, '0');
|
|
39
|
+
return i === 1 ? ver(a, b + 1, 0, '0') : ver(a, b, c + 1, '0');
|
|
40
|
+
};
|
|
41
|
+
const span = (p, i) => [
|
|
42
|
+
{ op: '>=', v: lower(p) },
|
|
43
|
+
{ op: '<', v: bump(p, i) },
|
|
44
|
+
];
|
|
45
|
+
const exact = (p) => p.nums.length === 3 ? [{ op: '=', v: lower(p) }] : span(p, p.nums.length - 1);
|
|
46
|
+
const caretIndex = (nums) => {
|
|
47
|
+
const i = nums.findIndex((n) => n !== 0);
|
|
48
|
+
return i === -1 ? nums.length - 1 : i;
|
|
49
|
+
};
|
|
50
|
+
const bounded = (op, p) => {
|
|
51
|
+
const n = p.nums.length;
|
|
52
|
+
if (op === '>=')
|
|
53
|
+
return [{ op, v: lower(p) }];
|
|
54
|
+
if (op === '>')
|
|
55
|
+
return n === 3 ? [{ op, v: lower(p) }] : [{ op: '>=', v: bump(p, n - 1) }];
|
|
56
|
+
if (op === '<')
|
|
57
|
+
return [{ op, v: n === 3 ? lower(p) : { ...lower(p), pre: '0' } }];
|
|
58
|
+
return n === 3 ? [{ op, v: lower(p) }] : [{ op: '<', v: bump(p, n - 1) }];
|
|
59
|
+
};
|
|
60
|
+
const desugar = (op, p) => {
|
|
61
|
+
if (p.nums.length === 0)
|
|
62
|
+
return op === '>' || op === '<' ? null : [];
|
|
63
|
+
if (op === undefined || op === '=')
|
|
64
|
+
return exact(p);
|
|
65
|
+
if (op === '~')
|
|
66
|
+
return span(p, Math.min(p.nums.length - 1, 1));
|
|
67
|
+
if (op === '^')
|
|
68
|
+
return span(p, caretIndex(p.nums));
|
|
69
|
+
return bounded(op, p);
|
|
70
|
+
};
|
|
71
|
+
const parseComparator = (token) => {
|
|
72
|
+
const m = COMPARATOR_RE.exec(token);
|
|
73
|
+
const partial = m?.[2] === undefined ? null : parsePartial(m[2]);
|
|
74
|
+
return m && partial ? desugar(m[1], partial) : null;
|
|
75
|
+
};
|
|
76
|
+
const parseSet = (set) => {
|
|
77
|
+
const tokens = set
|
|
78
|
+
.replace(/(\^|~|>=|<=|>|<|=)\s+/g, '$1')
|
|
79
|
+
.trim()
|
|
80
|
+
.split(/\s+/);
|
|
81
|
+
if (tokens.length === 0 || tokens[0] === '')
|
|
82
|
+
return null;
|
|
83
|
+
const parsed = tokens.map(parseComparator);
|
|
84
|
+
return parsed.some((c) => c === null) ? null : parsed.flatMap((c) => c ?? []);
|
|
85
|
+
};
|
|
86
|
+
export const parseRange = (range) => {
|
|
87
|
+
const sets = range.split('||').map(parseSet);
|
|
88
|
+
return sets.some((s) => s === null) ? null : sets.map((s) => s ?? []);
|
|
89
|
+
};
|
|
90
|
+
export const isRange = (range) => parseRange(range) !== null;
|
|
91
|
+
// Prereleases sort below their release, compared as plain strings (no identifier splitting).
|
|
92
|
+
const comparePre = (a, b) => {
|
|
93
|
+
if (a === b)
|
|
94
|
+
return 0;
|
|
95
|
+
if (a === null || b === null)
|
|
96
|
+
return a === null ? 1 : -1;
|
|
97
|
+
return a < b ? -1 : 1;
|
|
98
|
+
};
|
|
99
|
+
const compareVersions = (a, b) => a.major - b.major || a.minor - b.minor || a.patch - b.patch || comparePre(a.pre, b.pre);
|
|
100
|
+
const holds = (c, v) => {
|
|
101
|
+
const d = compareVersions(v, c.v);
|
|
102
|
+
if (c.op === '=')
|
|
103
|
+
return d === 0;
|
|
104
|
+
if (c.op === '<')
|
|
105
|
+
return d < 0;
|
|
106
|
+
if (c.op === '<=')
|
|
107
|
+
return d <= 0;
|
|
108
|
+
return c.op === '>' ? d > 0 : d >= 0;
|
|
109
|
+
};
|
|
110
|
+
export const satisfies = (version, range) => {
|
|
111
|
+
const v = parseVersion(version);
|
|
112
|
+
const sets = parseRange(range);
|
|
113
|
+
if (!v || !sets)
|
|
114
|
+
return false;
|
|
115
|
+
return sets.some((set) => set.every((c) => holds(c, v)));
|
|
116
|
+
};
|
|
117
|
+
// For sort(): newest version first; text that is not a version goes last.
|
|
118
|
+
export const newestFirst = (a, b) => {
|
|
119
|
+
const x = parseVersion(a);
|
|
120
|
+
const y = parseVersion(b);
|
|
121
|
+
if (x && y)
|
|
122
|
+
return compareVersions(y, x);
|
|
123
|
+
return (x ? -1 : 0) + (y ? 1 : 0);
|
|
124
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { type ExtensionApi, type ExtensionContext } from './extension.js';
|
|
3
|
+
export interface TestContextOverrides extends Partial<Omit<ExtensionContext, 'mode' | 'use'>> {
|
|
4
|
+
use?: Record<string, ExtensionApi>;
|
|
5
|
+
program?: Command;
|
|
6
|
+
}
|
|
7
|
+
export declare const createTestContext: (overrides?: TestContextOverrides) => ExtensionContext;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@agentage/cli/extension/test` subpath export: `createTestContext` builds the `ctx` an
|
|
3
|
+
* extension's own unit tests hand to its `register`, over a temp dir, with `use` from fakes.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdtempSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import {} from './extension.js';
|
|
10
|
+
const memorySecrets = () => {
|
|
11
|
+
const values = new Map();
|
|
12
|
+
return {
|
|
13
|
+
get: async (key) => values.get(key),
|
|
14
|
+
set: async (key, value) => void values.set(key, value),
|
|
15
|
+
delete: async (key) => void values.delete(key),
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
const fakeUse = (fakes) => ((id, options = {}) => {
|
|
19
|
+
const fake = fakes[id];
|
|
20
|
+
if (fake !== undefined || options.optional)
|
|
21
|
+
return fake;
|
|
22
|
+
throw new Error(`use("${id}"): no fake for it in createTestContext({ use })`);
|
|
23
|
+
});
|
|
24
|
+
// A fresh temp dir per context, laid out like the CLI's config dir.
|
|
25
|
+
const folders = (id) => {
|
|
26
|
+
const root = mkdtempSync(join(tmpdir(), 'agentage-test-'));
|
|
27
|
+
return { data: join(root, 'data', id), cache: join(root, 'cache', id) };
|
|
28
|
+
};
|
|
29
|
+
// Same members, in the same order, as the ctx the CLI builds; mode is always 'test'.
|
|
30
|
+
export const createTestContext = (overrides = {}) => {
|
|
31
|
+
const { use = {}, program = new Command('agentage'), config = {}, ...rest } = overrides;
|
|
32
|
+
const id = rest.id ?? 'test';
|
|
33
|
+
const subscriptions = [];
|
|
34
|
+
const ctx = {
|
|
35
|
+
id,
|
|
36
|
+
version: '0.0.0',
|
|
37
|
+
previousVersion: null,
|
|
38
|
+
mode: 'test',
|
|
39
|
+
config: Object.freeze({ ...config }),
|
|
40
|
+
secrets: memorySecrets(),
|
|
41
|
+
...folders(id),
|
|
42
|
+
log: () => undefined,
|
|
43
|
+
commands: { add: (...registrars) => registrars.forEach((register) => register(program)) },
|
|
44
|
+
use: fakeUse(use),
|
|
45
|
+
subscriptions: { push: (...items) => void subscriptions.push(...items) },
|
|
46
|
+
...rest,
|
|
47
|
+
};
|
|
48
|
+
return Object.freeze(ctx);
|
|
49
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type Command } from 'commander';
|
|
2
|
+
export declare const EXTENSION_API_VERSION = "1.0.0";
|
|
3
|
+
export type ConfigType = 'string' | 'number' | 'boolean' | 'array' | 'object';
|
|
4
|
+
export interface ConfigurationEntry {
|
|
5
|
+
type: ConfigType;
|
|
6
|
+
default?: unknown;
|
|
7
|
+
description?: string;
|
|
8
|
+
enum?: unknown[];
|
|
9
|
+
secret?: boolean;
|
|
10
|
+
scope?: 'shared' | 'machine';
|
|
11
|
+
}
|
|
12
|
+
export interface ExtensionManifest {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
keywords?: string[];
|
|
16
|
+
engines?: {
|
|
17
|
+
agentage?: string;
|
|
18
|
+
};
|
|
19
|
+
peerDependencies?: Record<string, string>;
|
|
20
|
+
peerDependenciesMeta?: Record<string, {
|
|
21
|
+
optional?: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
agentage: {
|
|
24
|
+
id: string;
|
|
25
|
+
contributes?: {
|
|
26
|
+
commands?: string[];
|
|
27
|
+
};
|
|
28
|
+
configuration?: Record<string, ConfigurationEntry>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface Disposable {
|
|
32
|
+
dispose: () => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export type ExtensionApi = Readonly<Record<string, (...args: never[]) => unknown>>;
|
|
35
|
+
export interface ExtensionApis {
|
|
36
|
+
}
|
|
37
|
+
export interface ExtensionUse {
|
|
38
|
+
<K extends keyof ExtensionApis>(id: K, options?: {
|
|
39
|
+
optional?: false;
|
|
40
|
+
}): ExtensionApis[K];
|
|
41
|
+
<K extends keyof ExtensionApis>(id: K, options: {
|
|
42
|
+
optional: true;
|
|
43
|
+
}): ExtensionApis[K] | undefined;
|
|
44
|
+
(id: string, options?: {
|
|
45
|
+
optional?: false;
|
|
46
|
+
}): ExtensionApi;
|
|
47
|
+
(id: string, options: {
|
|
48
|
+
optional: true;
|
|
49
|
+
}): ExtensionApi | undefined;
|
|
50
|
+
}
|
|
51
|
+
export interface ExtensionSecrets {
|
|
52
|
+
get: (key: string) => Promise<string | undefined>;
|
|
53
|
+
set: (key: string, value: string) => Promise<void>;
|
|
54
|
+
delete: (key: string) => Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export type ExtensionLog = (message: string, fields?: Record<string, unknown>) => void;
|
|
57
|
+
export type CommandRegistrar = (program: Command) => void;
|
|
58
|
+
export interface ExtensionCommands {
|
|
59
|
+
add: (...registrars: CommandRegistrar[]) => void;
|
|
60
|
+
}
|
|
61
|
+
export interface ExtensionSubscriptions {
|
|
62
|
+
push: (...disposables: Disposable[]) => void;
|
|
63
|
+
}
|
|
64
|
+
export type ExtensionMode = 'installed' | 'dev' | 'test';
|
|
65
|
+
export interface ExtensionContext {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly version: string;
|
|
68
|
+
readonly previousVersion: string | null;
|
|
69
|
+
readonly mode: ExtensionMode;
|
|
70
|
+
readonly config: Readonly<Record<string, unknown>>;
|
|
71
|
+
readonly secrets: ExtensionSecrets;
|
|
72
|
+
readonly data: string;
|
|
73
|
+
readonly cache: string;
|
|
74
|
+
readonly log: ExtensionLog;
|
|
75
|
+
readonly commands: ExtensionCommands;
|
|
76
|
+
readonly use: ExtensionUse;
|
|
77
|
+
readonly subscriptions: ExtensionSubscriptions;
|
|
78
|
+
}
|
|
79
|
+
export type Register = (ctx: ExtensionContext) => ExtensionApi | void | Promise<ExtensionApi | void>;
|
|
80
|
+
export interface Extension {
|
|
81
|
+
register: Register;
|
|
82
|
+
}
|
|
83
|
+
export declare const defineExtension: <T extends Extension>(extension: T) => T;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Types for an extension's entry; at runtime an extension gets everything through ctx, never this.
|
|
2
|
+
import {} from 'commander';
|
|
3
|
+
// The extension contract is versioned apart from the CLI, so extensions pin it, not a CLI release.
|
|
4
|
+
export const EXTENSION_API_VERSION = '1.0.0';
|
|
5
|
+
// Identity at runtime: it only gives an entry its types.
|
|
6
|
+
export const defineExtension = (extension) => extension;
|
package/dist/lib/auth/api.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type AuthState } from '../fs/config.js';
|
|
2
|
-
import { type CliRequestInit } from '../net/fetch.js';
|
|
2
|
+
import { type CliFetchOptions, type CliRequestInit } from '../net/fetch.js';
|
|
3
3
|
import { type Links } from '../net/origins.js';
|
|
4
4
|
export { AuthRequiredError, TransientAuthError } from './auth-errors.js';
|
|
5
5
|
export declare const refreshOrThrow: (auth: AuthState, links: Links) => Promise<void>;
|
|
6
|
-
export declare const authedFetch: (auth: AuthState, links: Links, url: string, init?: CliRequestInit) => Promise<Response>;
|
|
6
|
+
export declare const authedFetch: (auth: AuthState, links: Links, url: string, init?: CliRequestInit, opts?: CliFetchOptions) => Promise<Response>;
|
|
7
7
|
export declare const authedJson: <T>(auth: AuthState, links: Links, url: string, init?: CliRequestInit) => Promise<T>;
|
|
8
8
|
export { introspectToken, type TokenSession } from './introspect.js';
|
package/dist/lib/auth/api.js
CHANGED
|
@@ -61,12 +61,12 @@ const withBearer = (auth, init) => ({
|
|
|
61
61
|
headers: { ...init.headers, authorization: `Bearer ${auth.tokens.accessToken}` },
|
|
62
62
|
});
|
|
63
63
|
// The raw Response after at most one refresh-and-retry on 401; callers branch on the status.
|
|
64
|
-
export const authedFetch = async (auth, links, url, init = {}) => {
|
|
65
|
-
const res = await cliFetch(url, withBearer(auth, init));
|
|
64
|
+
export const authedFetch = async (auth, links, url, init = {}, opts = {}) => {
|
|
65
|
+
const res = await cliFetch(url, withBearer(auth, init), opts);
|
|
66
66
|
if (res.status !== 401 || !(await tryRefresh(auth, links)))
|
|
67
67
|
return res;
|
|
68
68
|
await res.body?.cancel();
|
|
69
|
-
return cliFetch(url, withBearer(auth, init));
|
|
69
|
+
return cliFetch(url, withBearer(auth, init), opts);
|
|
70
70
|
};
|
|
71
71
|
export const authedJson = async (auth, links, url, init = {}) => {
|
|
72
72
|
const method = init.method ?? 'GET';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The one page the CLI ever serves: the sign-in result, on the design-system dark tokens.
|
|
2
|
+
const LOGO = 'M143.8 33.7a13.2 13.2 0 00-23.6 0l-92.4 184.8a13.2 13.2 0 0015.4 18.6l66-18.9a13.2 13.2 0 ' +
|
|
3
|
+
'009.5-12.7v-60.3a13.2 13.2 0 1126.4 0v60.3a13.2 13.2 0 009.6 12.7l66 18.9a13.2 13.2 0 ' +
|
|
4
|
+
'0015.4-18.6l-92.3-184.8z';
|
|
5
|
+
const STYLE = `
|
|
6
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
7
|
+
html{color-scheme:dark}
|
|
8
|
+
body{background:oklch(0.085 0.013 277);color:oklch(0.97 0.003 277);
|
|
9
|
+
font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
|
10
|
+
-webkit-font-smoothing:antialiased;min-height:100vh;display:grid;place-items:center;padding:24px}
|
|
11
|
+
.card{background:oklch(0.17 0.018 277);border:1px solid oklch(0.237 0.017 277);border-radius:.75rem;
|
|
12
|
+
padding:40px 32px;width:100%;max-width:400px;text-align:center;
|
|
13
|
+
box-shadow:0 10px 15px -3px oklch(0 0 0/.1),0 4px 6px -4px oklch(0 0 0/.1)}
|
|
14
|
+
.brand{display:inline-flex;align-items:center;gap:10px;margin-bottom:28px;font-weight:600;font-size:1rem}
|
|
15
|
+
.brand svg{width:28px;height:28px}
|
|
16
|
+
.icon{width:48px;height:48px;border-radius:9999px;margin:0 auto 20px;display:grid;place-items:center}
|
|
17
|
+
.ok .icon{background:oklch(0.723 0.191 149.6/.15);color:oklch(0.723 0.191 149.6)}
|
|
18
|
+
.err .icon{background:oklch(0.628 0.208 25.6/.15);color:oklch(0.628 0.208 25.6)}
|
|
19
|
+
h1{font-size:1.25rem;font-weight:600;letter-spacing:-.02em;margin-bottom:8px}
|
|
20
|
+
p{color:oklch(0.59 0.014 256);font-size:.875rem;line-height:1.5}
|
|
21
|
+
.hint{margin-top:24px;font-size:.75rem;color:oklch(0.59 0.014 256)}
|
|
22
|
+
`;
|
|
23
|
+
const ICON = {
|
|
24
|
+
ok: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
|
|
25
|
+
err: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>',
|
|
26
|
+
};
|
|
27
|
+
const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
|
28
|
+
// The error text comes from the authorization server's redirect: never trust it as markup.
|
|
29
|
+
export const escapeHtml = (text) => text.replace(/[&<>"]/g, (c) => ESCAPES[c] ?? c);
|
|
30
|
+
export const callbackPage = (ok, detail) => {
|
|
31
|
+
const kind = ok ? 'ok' : 'err';
|
|
32
|
+
const title = ok ? 'Signed in' : 'Sign-in failed';
|
|
33
|
+
const hint = ok ? 'You can close this tab.' : 'Run agentage setup again to retry.';
|
|
34
|
+
return (`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">` +
|
|
35
|
+
`<title>${title} - agentage</title><style>${STYLE}</style></head><body><main class="card ${kind}">` +
|
|
36
|
+
`<div class="brand"><svg viewBox="0 0 416 416" aria-hidden="true"><rect width="416" height="416" rx="83" fill="#f3a52b"/>` +
|
|
37
|
+
`<g transform="translate(25.7 25.7) scale(1.3812)"><path d="${LOGO}" fill="#000"/></g></svg>agentage</div>` +
|
|
38
|
+
`<div class="icon">${ICON[kind]}</div><h1>${title}</h1><p>${escapeHtml(detail)}</p>` +
|
|
39
|
+
`<p class="hint">${hint}</p></main></body></html>`);
|
|
40
|
+
};
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
|
|
3
|
-
'<body style="font-family:system-ui;display:grid;place-items:center;min-height:80vh">' +
|
|
4
|
-
`<div style="text-align:center"><h1>${ok ? 'Signed in' : 'Sign-in failed'}</h1>` +
|
|
5
|
-
`<p>${detail}</p></div></body></html>`;
|
|
2
|
+
import { callbackPage as page } from './callback-page.js';
|
|
6
3
|
export const startCallbackServer = (expectedState) => new Promise((resolveServer) => {
|
|
7
4
|
let settle = null;
|
|
8
5
|
let result = null;
|
|
@@ -2,7 +2,9 @@ export interface LockOptions {
|
|
|
2
2
|
ttlMs?: number;
|
|
3
3
|
hardStaleMs?: number;
|
|
4
4
|
maxWaitMs?: number;
|
|
5
|
+
onWait?: () => void;
|
|
5
6
|
}
|
|
6
7
|
export declare const acquireFileLock: (target: string, now?: number, opts?: LockOptions) => boolean;
|
|
7
8
|
export declare const releaseFileLock: (target: string) => void;
|
|
8
|
-
export declare const
|
|
9
|
+
export declare const refreshFileLock: (target: string, now?: number) => boolean;
|
|
10
|
+
export declare const withFileLock: <T>(target: string, fn: () => T | Promise<T>, opts?: LockOptions) => Promise<T>;
|