recess-cli 1.9.2 → 2.0.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 +82 -55
- package/dist/api.js +42 -10
- package/dist/args.js +12 -1
- package/dist/auth.js +11 -10
- package/dist/cli.js +1190 -245
- package/dist/command-schema.js +257 -0
- package/dist/config.js +116 -19
- package/dist/delivery.js +26 -0
- package/dist/feedback.js +64 -0
- package/dist/http.js +25 -0
- package/dist/index.js +15 -3
- package/dist/jobs.js +80 -0
- package/dist/skill-update.js +2 -1
- package/dist/ui/index.js +2 -1
- package/package.json +1 -1
- package/skill/recess-cli/SKILL.md +18 -8
- package/skill/recess-cli/agents/version.json +2 -2
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { CliError } from "./errors.js";
|
|
2
|
+
export const AGENT_CONTEXT_SCHEMA_VERSION = "3";
|
|
3
|
+
const BOOLEAN_FLAGS = new Set([
|
|
4
|
+
"all-references",
|
|
5
|
+
"allow-strand",
|
|
6
|
+
"archived",
|
|
7
|
+
"cancel-subscriptions",
|
|
8
|
+
"confirm",
|
|
9
|
+
"confirm-destructive-changes",
|
|
10
|
+
"disable-applet-follow-ups",
|
|
11
|
+
"dry-run",
|
|
12
|
+
"enable-applet-follow-ups",
|
|
13
|
+
"full",
|
|
14
|
+
"help",
|
|
15
|
+
"immediate",
|
|
16
|
+
"include-deleted",
|
|
17
|
+
"json",
|
|
18
|
+
"mirrored",
|
|
19
|
+
"no-collision",
|
|
20
|
+
"no-invite",
|
|
21
|
+
"refresh",
|
|
22
|
+
"restore",
|
|
23
|
+
"revoke",
|
|
24
|
+
"send-email",
|
|
25
|
+
"skill-only",
|
|
26
|
+
"spec-only",
|
|
27
|
+
"starter-only",
|
|
28
|
+
"visual-only",
|
|
29
|
+
"wait",
|
|
30
|
+
]);
|
|
31
|
+
const REPEATABLE_FLAGS = new Set(["kid", "unassign"]);
|
|
32
|
+
const GLOBAL_FLAGS = new Set([
|
|
33
|
+
"deliver",
|
|
34
|
+
"help",
|
|
35
|
+
"json",
|
|
36
|
+
"operation-key",
|
|
37
|
+
"profile",
|
|
38
|
+
"reason",
|
|
39
|
+
]);
|
|
40
|
+
const GLOBAL_FLAG_TYPES = {
|
|
41
|
+
deliver: "string",
|
|
42
|
+
help: "boolean",
|
|
43
|
+
json: "boolean",
|
|
44
|
+
"operation-key": "string",
|
|
45
|
+
profile: "string",
|
|
46
|
+
reason: "string",
|
|
47
|
+
};
|
|
48
|
+
function usageBlocks(help) {
|
|
49
|
+
const lines = help.split("\n");
|
|
50
|
+
const blocks = [];
|
|
51
|
+
let current = null;
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
if (line.startsWith(" recess ")) {
|
|
54
|
+
if (current)
|
|
55
|
+
blocks.push(current.join(" ").replace(/\s+/g, " ").trim());
|
|
56
|
+
current = [line.trim()];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (current && /^\s{4,}\S/.test(line)) {
|
|
60
|
+
current.push(line.trim());
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (current) {
|
|
64
|
+
blocks.push(current.join(" ").replace(/\s+/g, " ").trim());
|
|
65
|
+
current = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (current)
|
|
69
|
+
blocks.push(current.join(" ").replace(/\s+/g, " ").trim());
|
|
70
|
+
return blocks;
|
|
71
|
+
}
|
|
72
|
+
function commandPath(usage) {
|
|
73
|
+
const rest = usage.replace(/^recess\s+(?:\[--json\]\s+)?/, "");
|
|
74
|
+
const tokens = rest.split(/\s+/);
|
|
75
|
+
const path = [];
|
|
76
|
+
for (const token of tokens) {
|
|
77
|
+
if (token.startsWith("--") ||
|
|
78
|
+
token.startsWith("[") ||
|
|
79
|
+
token.startsWith("<") ||
|
|
80
|
+
token.startsWith("(") ||
|
|
81
|
+
token.startsWith('"')) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
path.push(token);
|
|
85
|
+
}
|
|
86
|
+
if (path.length === 0 && rest.startsWith("--version"))
|
|
87
|
+
return [["--version"]];
|
|
88
|
+
let variants = [[]];
|
|
89
|
+
for (const token of path) {
|
|
90
|
+
const choices = token.split("|");
|
|
91
|
+
variants = variants.flatMap((prefix) => choices.map((choice) => [...prefix, choice]));
|
|
92
|
+
}
|
|
93
|
+
return variants;
|
|
94
|
+
}
|
|
95
|
+
function flagValues(usage, name) {
|
|
96
|
+
const match = usage.match(new RegExp(`--${name}(?:[ =])([A-Za-z0-9_-]+(?:\\|[A-Za-z0-9_-]+)+)`));
|
|
97
|
+
return match?.[1]?.split("|");
|
|
98
|
+
}
|
|
99
|
+
function flagIsIndividuallyRequired(usage, name) {
|
|
100
|
+
const marker = `--${name}`;
|
|
101
|
+
let squareDepth = 0;
|
|
102
|
+
let parenDepth = 0;
|
|
103
|
+
for (let index = 0; index < usage.length; index += 1) {
|
|
104
|
+
const character = usage[index];
|
|
105
|
+
if (character === "[")
|
|
106
|
+
squareDepth += 1;
|
|
107
|
+
if (character === "]")
|
|
108
|
+
squareDepth = Math.max(0, squareDepth - 1);
|
|
109
|
+
if (character === "(")
|
|
110
|
+
parenDepth += 1;
|
|
111
|
+
if (character === ")")
|
|
112
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
113
|
+
if (usage.startsWith(marker, index) &&
|
|
114
|
+
!/[a-z0-9-]/.test(usage[index + marker.length] ?? "") &&
|
|
115
|
+
squareDepth === 0 &&
|
|
116
|
+
parenDepth === 0) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function positionalBounds(usage) {
|
|
123
|
+
const rest = usage.replace(/^recess\s+(?:\[--json\]\s+)?/, "");
|
|
124
|
+
const tokens = rest.split(/\s+/);
|
|
125
|
+
let count = 0;
|
|
126
|
+
let variadic = false;
|
|
127
|
+
for (const token of tokens) {
|
|
128
|
+
if (token.startsWith("--") ||
|
|
129
|
+
token.startsWith("[") ||
|
|
130
|
+
token.startsWith("(")) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
count += 1;
|
|
134
|
+
if (/^<[^,>]+\.\.\.>$/.test(token))
|
|
135
|
+
variadic = true;
|
|
136
|
+
}
|
|
137
|
+
return { min: count, max: variadic ? null : count };
|
|
138
|
+
}
|
|
139
|
+
export function buildCommandSchema(help) {
|
|
140
|
+
return usageBlocks(help).flatMap((usage) => {
|
|
141
|
+
const names = Array.from(usage.matchAll(/--([a-z][a-z0-9-]*)/g)).map((match) => match[1]);
|
|
142
|
+
const flags = Object.fromEntries(Array.from(new Set(names)).map((name) => [
|
|
143
|
+
name,
|
|
144
|
+
{
|
|
145
|
+
type: BOOLEAN_FLAGS.has(name) ? "boolean" : "string",
|
|
146
|
+
repeatable: REPEATABLE_FLAGS.has(name),
|
|
147
|
+
required: flagIsIndividuallyRequired(usage, name),
|
|
148
|
+
...(flagValues(usage, name)
|
|
149
|
+
? { values: flagValues(usage, name) }
|
|
150
|
+
: {}),
|
|
151
|
+
},
|
|
152
|
+
]));
|
|
153
|
+
const positionals = positionalBounds(usage);
|
|
154
|
+
return commandPath(usage).map((path) => ({
|
|
155
|
+
path,
|
|
156
|
+
usage,
|
|
157
|
+
flags,
|
|
158
|
+
positionals,
|
|
159
|
+
}));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function pathStartsWith(path, prefix) {
|
|
163
|
+
return prefix.every((part, index) => path[index] === part);
|
|
164
|
+
}
|
|
165
|
+
export function findCommandSchema(commands, positionals) {
|
|
166
|
+
return commands
|
|
167
|
+
.filter((command) => pathStartsWith(positionals, command.path))
|
|
168
|
+
.sort((left, right) => right.path.length - left.path.length)[0];
|
|
169
|
+
}
|
|
170
|
+
export function scopedHelp(help, commands, scope) {
|
|
171
|
+
if (scope.length === 0)
|
|
172
|
+
return help;
|
|
173
|
+
const matches = commands.filter((command) => pathStartsWith(command.path, scope));
|
|
174
|
+
if (matches.length === 0) {
|
|
175
|
+
throw new CliError("unknown_command", `Unknown command scope: ${scope.join(" ")}. Run \`recess --help\` for available commands.`);
|
|
176
|
+
}
|
|
177
|
+
return [
|
|
178
|
+
`recess ${scope.join(" ")} — command help`,
|
|
179
|
+
"",
|
|
180
|
+
"Usage:",
|
|
181
|
+
...Array.from(new Set(matches.map((command) => ` ${command.usage}`))),
|
|
182
|
+
].join("\n");
|
|
183
|
+
}
|
|
184
|
+
export function validateInvocation(parsed, commands) {
|
|
185
|
+
const command = findCommandSchema(commands, parsed.positionals);
|
|
186
|
+
if (!command) {
|
|
187
|
+
const scope = parsed.positionals.join(" ");
|
|
188
|
+
const first = parsed.positionals[0];
|
|
189
|
+
const suggestions = commands
|
|
190
|
+
.filter((candidate) => !first || candidate.path[0] === first)
|
|
191
|
+
.slice(0, 12)
|
|
192
|
+
.map((candidate) => candidate.path.join(" "));
|
|
193
|
+
throw new CliError("unknown_command", `Unknown command: ${scope || "(none)"}.`, 1, suggestions.length > 0 ? { suggestions } : undefined);
|
|
194
|
+
}
|
|
195
|
+
const allowed = new Set([...Object.keys(command.flags), ...GLOBAL_FLAGS]);
|
|
196
|
+
const unknown = Array.from(parsed.flags.keys()).filter((name) => name !== "version" && !allowed.has(name));
|
|
197
|
+
if (unknown.length > 0) {
|
|
198
|
+
const validFlags = Array.from(allowed)
|
|
199
|
+
.sort()
|
|
200
|
+
.map((name) => `--${name}`);
|
|
201
|
+
throw new CliError("unknown_flag", `Unknown flag${unknown.length === 1 ? "" : "s"}: ${unknown
|
|
202
|
+
.map((name) => `--${name}`)
|
|
203
|
+
.join(", ")}.`, 1, { usage: command.usage, validFlags });
|
|
204
|
+
}
|
|
205
|
+
const duplicate = Array.from(parsed.occurrences).find(([name, count]) => count > 1 && !command.flags[name]?.repeatable);
|
|
206
|
+
if (duplicate) {
|
|
207
|
+
throw new CliError("duplicate_flag", `--${duplicate[0]} may only be passed once.`, 1, { usage: command.usage });
|
|
208
|
+
}
|
|
209
|
+
const positionalCount = parsed.positionals.length;
|
|
210
|
+
if (positionalCount < command.positionals.min ||
|
|
211
|
+
(command.positionals.max !== null &&
|
|
212
|
+
positionalCount > command.positionals.max)) {
|
|
213
|
+
throw new CliError("invalid_arguments", `Wrong number of positional arguments for ${command.path.join(" ")}.`, 1, { usage: command.usage });
|
|
214
|
+
}
|
|
215
|
+
for (const [name, value] of parsed.flags) {
|
|
216
|
+
const type = command.flags[name]?.type ?? GLOBAL_FLAG_TYPES[name];
|
|
217
|
+
if (type === "boolean" && value !== true) {
|
|
218
|
+
throw new CliError("invalid_arguments", `--${name} is a boolean flag and does not take a value.`, 1, { usage: command.usage });
|
|
219
|
+
}
|
|
220
|
+
if (type === "string" && value === true) {
|
|
221
|
+
throw new CliError("invalid_arguments", `--${name} requires a value.`, 1, { usage: command.usage });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return command;
|
|
225
|
+
}
|
|
226
|
+
export function agentContext(commands, options) {
|
|
227
|
+
return {
|
|
228
|
+
schema_version: AGENT_CONTEXT_SCHEMA_VERSION,
|
|
229
|
+
cli_version: options.cliVersion,
|
|
230
|
+
commands: Object.fromEntries(commands
|
|
231
|
+
.filter((command) => command.path[0] !== "--version")
|
|
232
|
+
.map((command) => [
|
|
233
|
+
command.path.join(" "),
|
|
234
|
+
{
|
|
235
|
+
usage: command.usage,
|
|
236
|
+
flags: command.flags,
|
|
237
|
+
positionals: command.positionals,
|
|
238
|
+
},
|
|
239
|
+
])),
|
|
240
|
+
global_flags: {
|
|
241
|
+
"--json": { type: "boolean" },
|
|
242
|
+
"--profile": { type: "string" },
|
|
243
|
+
"--operation-key": { type: "string" },
|
|
244
|
+
"--reason": {
|
|
245
|
+
type: "string",
|
|
246
|
+
required_for: "every Recess API request except authentication",
|
|
247
|
+
max_length: 1024,
|
|
248
|
+
},
|
|
249
|
+
"--deliver": { type: "enum", values: ["stdout", "file:<path>"] },
|
|
250
|
+
"--help": { type: "boolean" },
|
|
251
|
+
},
|
|
252
|
+
available_profiles: options.availableProfiles,
|
|
253
|
+
jobs: { commands: ["jobs list", "jobs get", "jobs prune"] },
|
|
254
|
+
feedback: { upstream_configured: options.feedbackUpstreamConfigured },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=command-schema.js.map
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { CliError } from "./errors.js";
|
|
4
5
|
export const DEFAULT_OAUTH_CLIENT_ID = "c7e34138-18f9-45b1-a2fb-26a4e3a6d739";
|
|
5
6
|
export function defaultConfigPath() {
|
|
6
7
|
return (process.env.RECESS_CLI_CONFIG ??
|
|
@@ -16,32 +17,111 @@ export async function readStoredConfig(configPath = defaultConfigPath()) {
|
|
|
16
17
|
throw error;
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
|
-
export async function resolveConfig() {
|
|
20
|
+
export async function resolveConfig(profileName) {
|
|
20
21
|
const configPath = defaultConfigPath();
|
|
21
22
|
const stored = await readStoredConfig(configPath);
|
|
23
|
+
const selectedProfileName = profileName ?? process.env.RECESS_CLI_PROFILE ?? stored.activeProfile;
|
|
24
|
+
const profile = selectedProfileName
|
|
25
|
+
? stored.profiles?.[selectedProfileName]
|
|
26
|
+
: undefined;
|
|
27
|
+
if (selectedProfileName && !profile) {
|
|
28
|
+
throw new CliError("unknown_profile", `Unknown Recess CLI profile ${selectedProfileName}. Run \`recess --json profile list\`.`);
|
|
29
|
+
}
|
|
22
30
|
const envCookie = process.env.RECESS_CLI_COOKIE;
|
|
23
|
-
const sessionCookie = envCookie ??
|
|
31
|
+
const sessionCookie = envCookie ??
|
|
32
|
+
(selectedProfileName ? profile?.sessionCookie : stored.sessionCookie);
|
|
24
33
|
return {
|
|
25
34
|
configPath,
|
|
26
35
|
apiOrigin: process.env.RECESS_CLI_API_ORIGIN ??
|
|
36
|
+
profile?.apiOrigin ??
|
|
27
37
|
stored.apiOrigin ??
|
|
28
38
|
"https://api.recess.gg",
|
|
29
39
|
webOrigin: process.env.RECESS_CLI_WEB_ORIGIN ??
|
|
40
|
+
profile?.webOrigin ??
|
|
30
41
|
stored.webOrigin ??
|
|
31
42
|
"https://recess.gg",
|
|
32
43
|
oauthClientId: process.env.RECESS_CLI_OAUTH_CLIENT_ID ??
|
|
44
|
+
profile?.oauthClientId ??
|
|
33
45
|
stored.oauthClientId ??
|
|
34
46
|
DEFAULT_OAUTH_CLIENT_ID,
|
|
35
47
|
sessionCookie,
|
|
36
|
-
sessionExpiresAt:
|
|
37
|
-
|
|
48
|
+
sessionExpiresAt: selectedProfileName
|
|
49
|
+
? profile?.sessionExpiresAt
|
|
50
|
+
: stored.sessionExpiresAt,
|
|
51
|
+
user: selectedProfileName ? profile?.user : stored.user,
|
|
38
52
|
authSource: envCookie ? "env" : sessionCookie ? "config" : "missing",
|
|
39
|
-
pendingDeviceCode:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
pendingDeviceCode: selectedProfileName
|
|
54
|
+
? profile?.pendingDeviceCode
|
|
55
|
+
: stored.pendingDeviceCode,
|
|
56
|
+
pendingUserCode: selectedProfileName
|
|
57
|
+
? profile?.pendingUserCode
|
|
58
|
+
: stored.pendingUserCode,
|
|
59
|
+
pendingApprovalUrl: selectedProfileName
|
|
60
|
+
? profile?.pendingApprovalUrl
|
|
61
|
+
: stored.pendingApprovalUrl,
|
|
62
|
+
pendingExpiresAt: selectedProfileName
|
|
63
|
+
? profile?.pendingExpiresAt
|
|
64
|
+
: stored.pendingExpiresAt,
|
|
65
|
+
profileName: selectedProfileName,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function assertProfileName(name) {
|
|
69
|
+
const normalized = name.trim();
|
|
70
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(normalized)) {
|
|
71
|
+
throw new CliError("invalid_arguments", "Profile names must be 1-64 letters, numbers, underscores, or hyphens.");
|
|
72
|
+
}
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
export async function listProfiles() {
|
|
76
|
+
const stored = await readStoredConfig();
|
|
77
|
+
return {
|
|
78
|
+
activeProfile: stored.activeProfile ?? null,
|
|
79
|
+
profiles: Object.entries(stored.profiles ?? {})
|
|
80
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
81
|
+
.map(([name, config]) => ({
|
|
82
|
+
name,
|
|
83
|
+
config: {
|
|
84
|
+
apiOrigin: config.apiOrigin,
|
|
85
|
+
webOrigin: config.webOrigin,
|
|
86
|
+
oauthClientId: config.oauthClientId,
|
|
87
|
+
},
|
|
88
|
+
})),
|
|
43
89
|
};
|
|
44
90
|
}
|
|
91
|
+
export async function saveProfile(name, profile) {
|
|
92
|
+
const normalized = assertProfileName(name);
|
|
93
|
+
const stored = await readStoredConfig();
|
|
94
|
+
await writeStoredConfig({
|
|
95
|
+
...stored,
|
|
96
|
+
profiles: {
|
|
97
|
+
...stored.profiles,
|
|
98
|
+
[normalized]: {
|
|
99
|
+
...stored.profiles?.[normalized],
|
|
100
|
+
...profile,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export async function useProfile(name) {
|
|
106
|
+
const normalized = assertProfileName(name);
|
|
107
|
+
const stored = await readStoredConfig();
|
|
108
|
+
if (!stored.profiles?.[normalized]) {
|
|
109
|
+
throw new CliError("unknown_profile", `Unknown Recess CLI profile ${normalized}. Run \`recess --json profile list\`.`);
|
|
110
|
+
}
|
|
111
|
+
await writeStoredConfig({ ...stored, activeProfile: normalized });
|
|
112
|
+
}
|
|
113
|
+
export async function deleteProfile(name) {
|
|
114
|
+
const normalized = assertProfileName(name);
|
|
115
|
+
const stored = await readStoredConfig();
|
|
116
|
+
if (!stored.profiles?.[normalized]) {
|
|
117
|
+
throw new CliError("unknown_profile", `Unknown Recess CLI profile ${normalized}.`);
|
|
118
|
+
}
|
|
119
|
+
const profiles = { ...stored.profiles };
|
|
120
|
+
delete profiles[normalized];
|
|
121
|
+
if (stored.activeProfile === normalized)
|
|
122
|
+
delete stored.activeProfile;
|
|
123
|
+
await writeStoredConfig({ ...stored, profiles });
|
|
124
|
+
}
|
|
45
125
|
export async function writeStoredConfig(config, configPath = defaultConfigPath()) {
|
|
46
126
|
const directory = path.dirname(configPath);
|
|
47
127
|
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
@@ -52,28 +132,45 @@ export async function writeStoredConfig(config, configPath = defaultConfigPath()
|
|
|
52
132
|
await fs.rename(temporary, configPath);
|
|
53
133
|
await fs.chmod(configPath, 0o600);
|
|
54
134
|
}
|
|
55
|
-
export async function updateStoredConfig(update) {
|
|
135
|
+
export async function updateStoredConfig(update, profileName) {
|
|
56
136
|
const configPath = defaultConfigPath();
|
|
57
137
|
const current = await readStoredConfig(configPath);
|
|
58
|
-
const next =
|
|
138
|
+
const next = profileName
|
|
139
|
+
? {
|
|
140
|
+
...current,
|
|
141
|
+
profiles: {
|
|
142
|
+
...current.profiles,
|
|
143
|
+
[assertProfileName(profileName)]: {
|
|
144
|
+
...current.profiles?.[profileName],
|
|
145
|
+
...update,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
: { ...current, ...update };
|
|
59
150
|
await writeStoredConfig(next, configPath);
|
|
60
151
|
return next;
|
|
61
152
|
}
|
|
62
|
-
export async function clearStoredSession() {
|
|
153
|
+
export async function clearStoredSession(profileName) {
|
|
63
154
|
const configPath = defaultConfigPath();
|
|
64
155
|
const current = await readStoredConfig(configPath);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
156
|
+
const target = profileName ? current.profiles?.[profileName] : current;
|
|
157
|
+
if (!target)
|
|
158
|
+
return;
|
|
159
|
+
delete target.sessionCookie;
|
|
160
|
+
delete target.sessionExpiresAt;
|
|
161
|
+
delete target.user;
|
|
68
162
|
await writeStoredConfig(current, configPath);
|
|
69
163
|
}
|
|
70
|
-
export async function clearPendingDeviceAuth() {
|
|
164
|
+
export async function clearPendingDeviceAuth(profileName) {
|
|
71
165
|
const configPath = defaultConfigPath();
|
|
72
166
|
const current = await readStoredConfig(configPath);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
delete
|
|
167
|
+
const target = profileName ? current.profiles?.[profileName] : current;
|
|
168
|
+
if (!target)
|
|
169
|
+
return;
|
|
170
|
+
delete target.pendingDeviceCode;
|
|
171
|
+
delete target.pendingUserCode;
|
|
172
|
+
delete target.pendingApprovalUrl;
|
|
173
|
+
delete target.pendingExpiresAt;
|
|
77
174
|
await writeStoredConfig(current, configPath);
|
|
78
175
|
}
|
|
79
176
|
//# sourceMappingURL=config.js.map
|
package/dist/delivery.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CliError } from "./errors.js";
|
|
4
|
+
export async function deliverJson(destination, envelope) {
|
|
5
|
+
if (!destination || destination === "stdout")
|
|
6
|
+
return { delivered: false };
|
|
7
|
+
if (!destination.startsWith("file:")) {
|
|
8
|
+
throw new CliError("invalid_delivery", "--deliver must be one of: stdout, file:<path>. Webhook delivery is deliberately unavailable for authenticated Recess data.");
|
|
9
|
+
}
|
|
10
|
+
const requestedPath = destination.slice("file:".length);
|
|
11
|
+
if (!requestedPath) {
|
|
12
|
+
throw new CliError("invalid_delivery", "file: delivery requires a path.");
|
|
13
|
+
}
|
|
14
|
+
const absolutePath = path.resolve(requestedPath);
|
|
15
|
+
const bytes = Buffer.from(`${JSON.stringify(envelope)}\n`, "utf8");
|
|
16
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true, mode: 0o700 });
|
|
17
|
+
const temporary = `${absolutePath}.${process.pid}.tmp`;
|
|
18
|
+
await fs.writeFile(temporary, bytes, { mode: 0o600 });
|
|
19
|
+
await fs.rename(temporary, absolutePath);
|
|
20
|
+
return {
|
|
21
|
+
delivered: true,
|
|
22
|
+
deliveredTo: `file:${absolutePath}`,
|
|
23
|
+
bytes: bytes.byteLength,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=delivery.js.map
|
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { CliError } from "./errors.js";
|
|
5
|
+
function feedbackPath() {
|
|
6
|
+
return (process.env.RECESS_CLI_FEEDBACK_PATH ??
|
|
7
|
+
path.join(os.homedir(), ".recess-cli", "feedback.jsonl"));
|
|
8
|
+
}
|
|
9
|
+
async function readFeedback() {
|
|
10
|
+
try {
|
|
11
|
+
return (await fs.readFile(feedbackPath(), "utf8"))
|
|
12
|
+
.split("\n")
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.flatMap((line) => {
|
|
15
|
+
try {
|
|
16
|
+
return [JSON.parse(line)];
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === "ENOENT")
|
|
25
|
+
return [];
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function listFeedback(limit = 20) {
|
|
30
|
+
const entries = (await readFeedback()).sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
31
|
+
return {
|
|
32
|
+
entries: entries.slice(0, limit),
|
|
33
|
+
truncated: entries.length > limit,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export async function submitFeedback(entry) {
|
|
37
|
+
const existing = (await readFeedback()).find((item) => item.id === entry.id);
|
|
38
|
+
if (existing)
|
|
39
|
+
return existing;
|
|
40
|
+
const endpoint = process.env.RECESS_CLI_FEEDBACK_ENDPOINT;
|
|
41
|
+
let upstreamStatus;
|
|
42
|
+
if (endpoint) {
|
|
43
|
+
const response = await fetch(endpoint, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
"content-type": "application/json",
|
|
47
|
+
"idempotency-key": entry.id,
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify(entry),
|
|
50
|
+
signal: AbortSignal.timeout(10_000),
|
|
51
|
+
});
|
|
52
|
+
upstreamStatus = response.status;
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new CliError("feedback_delivery_failed", `Feedback endpoint returned HTTP ${response.status}. Retry the unchanged command with the same operation key.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const saved = { ...entry, ...(upstreamStatus ? { upstreamStatus } : {}) };
|
|
58
|
+
const file = feedbackPath();
|
|
59
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
60
|
+
await fs.appendFile(file, `${JSON.stringify(saved)}\n`, { mode: 0o600 });
|
|
61
|
+
await fs.chmod(file, 0o600);
|
|
62
|
+
return saved;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=feedback.js.map
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CliError } from "./errors.js";
|
|
2
|
+
export const RECESS_CLIENT_HEADER = "x-recess-client";
|
|
3
|
+
export const RECESS_CLIENT_CLI = "cli";
|
|
4
|
+
export const RECESS_CLIENT_CLI_UI = "cli-ui";
|
|
5
|
+
export const RECESS_REASON_HEADER = "x-recess-reason";
|
|
6
|
+
export function requireCliRequestReason(value) {
|
|
7
|
+
const reason = value?.trim();
|
|
8
|
+
if (!reason) {
|
|
9
|
+
throw new CliError("invalid_arguments", "Missing required --reason.");
|
|
10
|
+
}
|
|
11
|
+
if (reason.length > 1024) {
|
|
12
|
+
throw new CliError("invalid_arguments", "--reason must be 1024 characters or fewer.");
|
|
13
|
+
}
|
|
14
|
+
return reason;
|
|
15
|
+
}
|
|
16
|
+
export function markCliRequest(headers, reason, client = RECESS_CLIENT_CLI) {
|
|
17
|
+
headers.set(RECESS_CLIENT_HEADER, client);
|
|
18
|
+
if (reason)
|
|
19
|
+
headers.set(RECESS_REASON_HEADER, reason);
|
|
20
|
+
return headers;
|
|
21
|
+
}
|
|
22
|
+
export function cliRequestHeaders(init, reason, client = RECESS_CLIENT_CLI) {
|
|
23
|
+
return markCliRequest(new Headers(init), reason, client);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=http.js.map
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { parseArgs } from "./args.js";
|
|
4
4
|
import { runCommand } from "./cli.js";
|
|
5
|
+
import { deliverJson } from "./delivery.js";
|
|
5
6
|
import { CliError } from "./errors.js";
|
|
6
7
|
async function withInteractiveAdmissionStage(args) {
|
|
7
8
|
const parsed = parseArgs(args);
|
|
@@ -38,7 +39,8 @@ async function withInteractiveAdmissionStage(args) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
const argv = await withInteractiveAdmissionStage(process.argv.slice(2));
|
|
41
|
-
const
|
|
42
|
+
const parsedArgv = parseArgs(argv);
|
|
43
|
+
const json = parsedArgv.flags.has("json");
|
|
42
44
|
// The interactive console owns the terminal, so it runs before the JSON
|
|
43
45
|
// envelope machinery rather than through it.
|
|
44
46
|
if (argv[0] === "ui") {
|
|
@@ -57,11 +59,21 @@ if (argv[0] === "ui") {
|
|
|
57
59
|
}
|
|
58
60
|
try {
|
|
59
61
|
const data = await runCommand(argv);
|
|
60
|
-
|
|
62
|
+
const destination = parsedArgv.flags.get("deliver");
|
|
63
|
+
if (destination === true) {
|
|
64
|
+
throw new CliError("invalid_delivery", "--deliver requires a value.");
|
|
65
|
+
}
|
|
66
|
+
const envelope = { ok: true, data };
|
|
67
|
+
const delivery = await deliverJson(destination, envelope);
|
|
68
|
+
if (delivery.delivered) {
|
|
69
|
+
const acknowledgement = { ok: true, data: delivery };
|
|
70
|
+
process.stdout.write(`${json ? JSON.stringify(acknowledgement) : JSON.stringify(acknowledgement.data, null, 2)}\n`);
|
|
71
|
+
}
|
|
72
|
+
else if (typeof data === "object" && data && "help" in data && !json) {
|
|
61
73
|
process.stdout.write(`${String(data.help)}\n`);
|
|
62
74
|
}
|
|
63
75
|
else if (json) {
|
|
64
|
-
process.stdout.write(`${JSON.stringify(
|
|
76
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
65
77
|
}
|
|
66
78
|
else {
|
|
67
79
|
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
package/dist/jobs.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function jobsPath() {
|
|
5
|
+
return (process.env.RECESS_CLI_JOBS_PATH ??
|
|
6
|
+
path.join(os.homedir(), ".recess-cli", "jobs.jsonl"));
|
|
7
|
+
}
|
|
8
|
+
async function readEvents() {
|
|
9
|
+
try {
|
|
10
|
+
return (await fs.readFile(jobsPath(), "utf8"))
|
|
11
|
+
.split("\n")
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.flatMap((line) => {
|
|
14
|
+
try {
|
|
15
|
+
return [JSON.parse(line)];
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === "ENOENT")
|
|
24
|
+
return [];
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function appendJobEvent(event) {
|
|
29
|
+
const file = jobsPath();
|
|
30
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
31
|
+
let stored = event;
|
|
32
|
+
const serialized = JSON.stringify(stored);
|
|
33
|
+
if (serialized.length > 64 * 1024 && event.result !== undefined) {
|
|
34
|
+
stored = {
|
|
35
|
+
...event,
|
|
36
|
+
result: {
|
|
37
|
+
omitted: true,
|
|
38
|
+
reason: "Result exceeded the 64 KiB local job-ledger limit.",
|
|
39
|
+
bytes: Buffer.byteLength(serialized),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
await fs.appendFile(file, `${JSON.stringify(stored)}\n`, { mode: 0o600 });
|
|
44
|
+
await fs.chmod(file, 0o600);
|
|
45
|
+
}
|
|
46
|
+
function foldJobs(events) {
|
|
47
|
+
const jobs = new Map();
|
|
48
|
+
for (const event of events) {
|
|
49
|
+
const prior = jobs.get(event.jobId);
|
|
50
|
+
jobs.set(event.jobId, { ...event, events: (prior?.events ?? 0) + 1 });
|
|
51
|
+
}
|
|
52
|
+
return Array.from(jobs.values()).sort((left, right) => right.timestamp.localeCompare(left.timestamp));
|
|
53
|
+
}
|
|
54
|
+
export async function listJobs(limit = 20) {
|
|
55
|
+
const jobs = foldJobs(await readEvents());
|
|
56
|
+
return {
|
|
57
|
+
jobs: jobs.slice(0, limit),
|
|
58
|
+
truncated: jobs.length > limit,
|
|
59
|
+
total: jobs.length,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function getJob(jobId) {
|
|
63
|
+
const history = (await readEvents()).filter((event) => event.jobId === jobId);
|
|
64
|
+
return { job: foldJobs(history)[0] ?? null, history };
|
|
65
|
+
}
|
|
66
|
+
export async function pruneJobs(olderThanDays) {
|
|
67
|
+
const events = await readEvents();
|
|
68
|
+
const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
|
|
69
|
+
const kept = events.filter((event) => Date.parse(event.timestamp) >= cutoff);
|
|
70
|
+
const file = jobsPath();
|
|
71
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
72
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
73
|
+
await fs.writeFile(temporary, kept.length > 0
|
|
74
|
+
? `${kept.map((event) => JSON.stringify(event)).join("\n")}\n`
|
|
75
|
+
: "", { mode: 0o600 });
|
|
76
|
+
await fs.rename(temporary, file);
|
|
77
|
+
await fs.chmod(file, 0o600);
|
|
78
|
+
return { removed: events.length - kept.length, remaining: kept.length };
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=jobs.js.map
|