agency-lang 0.13.4 → 0.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/lib/cli/local.d.ts +14 -3
- package/dist/lib/cli/local.js +62 -14
- package/dist/lib/cli/localFlag.d.ts +12 -0
- package/dist/lib/cli/localFlag.js +16 -0
- package/dist/lib/cli/remote/commands/deploy.d.ts +5 -1
- package/dist/lib/cli/remote/commands/deploy.js +4 -3
- package/dist/lib/cli/remote/commands/secrets.d.ts +36 -0
- package/dist/lib/cli/remote/commands/secrets.js +178 -0
- package/dist/lib/cli/remote/confirmation.d.ts +9 -0
- package/dist/lib/cli/remote/confirmation.js +24 -6
- package/dist/lib/cli/remote/secretsInput.d.ts +54 -0
- package/dist/lib/cli/remote/secretsInput.js +123 -0
- package/dist/lib/cli/schedule/cron.d.ts +1 -0
- package/dist/lib/cli/schedule/cron.js +17 -2
- package/dist/lib/cli/schedule/remote.d.ts +39 -0
- package/dist/lib/cli/schedule/remote.js +242 -0
- package/dist/lib/cli/statelog/accountClient.js +41 -54
- package/dist/lib/cli/statelog/jsonBody.d.ts +20 -0
- package/dist/lib/cli/statelog/jsonBody.js +49 -0
- package/dist/lib/cli/statelog/projectClient.js +38 -62
- package/dist/lib/cli/statelog/redact.d.ts +8 -0
- package/dist/lib/cli/statelog/redact.js +21 -0
- package/dist/lib/cli/statelog/schedulesClient.d.ts +44 -0
- package/dist/lib/cli/statelog/schedulesClient.js +100 -0
- package/dist/lib/cli/statelog/secretsClient.d.ts +21 -0
- package/dist/lib/cli/statelog/secretsClient.js +94 -0
- package/dist/lib/cli/statelog/serveClient.js +45 -44
- package/dist/lib/cli/statelog/spendTypes.d.ts +8 -0
- package/dist/lib/cli/statelog/spendTypes.js +8 -1
- package/dist/lib/cli/statelog/statelogRequest.d.ts +50 -0
- package/dist/lib/cli/statelog/statelogRequest.js +87 -0
- package/dist/lib/cli/statelog/uploadClient.js +30 -25
- package/dist/lib/constants.d.ts +34 -0
- package/dist/lib/constants.js +36 -0
- package/dist/lib/runtime/audioFormats.d.ts +8 -0
- package/dist/lib/runtime/audioFormats.js +16 -0
- package/dist/lib/runtime/deterministicClient.d.ts +11 -2
- package/dist/lib/runtime/deterministicClient.js +67 -4
- package/dist/lib/runtime/interrupts.js +2 -0
- package/dist/lib/runtime/invocationUsage.d.ts +15 -1
- package/dist/lib/runtime/invocationUsage.js +46 -9
- package/dist/lib/runtime/llmClient.d.ts +24 -0
- package/dist/lib/runtime/llmClient.js +42 -0
- package/dist/lib/runtime/localProvider.d.ts +76 -0
- package/dist/lib/runtime/localProvider.js +127 -0
- package/dist/lib/runtime/memory/manager.js +4 -2
- package/dist/lib/runtime/node.js +2 -0
- package/dist/lib/runtime/prompt.js +10 -3
- package/dist/lib/runtime/recordPaidUsage.js +5 -2
- package/dist/lib/runtime/redactForStatelog.js +10 -2
- package/dist/lib/runtime/runBatch.js +8 -0
- package/dist/lib/runtime/runner.js +15 -4
- package/dist/lib/runtime/state/globalStore.d.ts +35 -6
- package/dist/lib/runtime/state/globalStore.js +86 -8
- package/dist/lib/statelogClient.d.ts +54 -0
- package/dist/lib/statelogClient.js +71 -1
- package/dist/lib/stdlib/abortable.d.ts +1 -1
- package/dist/lib/stdlib/abortable.js +1 -1
- package/dist/lib/stdlib/aws/client.d.ts +49 -0
- package/dist/lib/stdlib/aws/client.js +100 -0
- package/dist/lib/stdlib/aws/credentials.d.ts +15 -0
- package/dist/lib/stdlib/aws/credentials.js +21 -0
- package/dist/lib/stdlib/aws/endpoints.d.ts +30 -0
- package/dist/lib/stdlib/aws/endpoints.js +73 -0
- package/dist/lib/stdlib/aws/errors.d.ts +14 -0
- package/dist/lib/stdlib/aws/errors.js +24 -0
- package/dist/lib/stdlib/aws/s3.d.ts +61 -0
- package/dist/lib/stdlib/aws/s3.js +224 -0
- package/dist/lib/stdlib/aws/sigv4.d.ts +51 -0
- package/dist/lib/stdlib/aws/sigv4.js +125 -0
- package/dist/lib/stdlib/aws/uri.d.ts +8 -0
- package/dist/lib/stdlib/aws/uri.js +30 -0
- package/dist/lib/stdlib/base64.d.ts +11 -0
- package/dist/lib/stdlib/base64.js +20 -0
- package/dist/lib/stdlib/builtins.js +9 -7
- package/dist/lib/stdlib/http.d.ts +10 -0
- package/dist/lib/stdlib/http.js +51 -21
- package/dist/lib/stdlib/localModelManifest.d.ts +14 -0
- package/dist/lib/stdlib/localModelManifest.js +56 -0
- package/dist/lib/stdlib/localModels.d.ts +26 -24
- package/dist/lib/stdlib/localModels.js +81 -112
- package/dist/lib/stdlib/mcpBridge.mjs +3 -2
- package/dist/lib/stdlib/mcpResolver.d.ts +1 -1
- package/dist/lib/stdlib/mcpResolver.js +2 -2
- package/dist/lib/stdlib/objectBytes.d.ts +3 -0
- package/dist/lib/stdlib/objectBytes.js +12 -0
- package/dist/lib/stdlib/speech.d.ts +30 -17
- package/dist/lib/stdlib/speech.js +288 -57
- package/dist/lib/stdlib/tag.d.ts +1 -1
- package/dist/lib/stdlib/tag.js +2 -2
- package/dist/lib/stdlib/thread.d.ts +6 -0
- package/dist/lib/stdlib/thread.js +12 -2
- package/dist/lib/typeChecker/builtins.js +16 -8
- package/dist/scripts/agency.js +216 -23
- package/package.json +2 -2
- package/stdlib/agents/data.agency +1 -1
- package/stdlib/agents/data.js +1 -1
- package/stdlib/aws/s3.agency +226 -0
- package/stdlib/aws/s3.js +1389 -0
- package/stdlib/capabilities.agency +8 -4
- package/stdlib/capabilities.js +4 -2
- package/stdlib/data/connector.agency +66 -0
- package/stdlib/data/connector.js +976 -0
- package/stdlib/data/finance/dbnomics.agency +5 -4
- package/stdlib/data/finance/dbnomics.js +17 -20
- package/stdlib/data/finance/edgar.agency +8 -7
- package/stdlib/data/finance/edgar.js +58 -73
- package/stdlib/data/finance/fred.agency +6 -5
- package/stdlib/data/finance/fred.js +31 -37
- package/stdlib/data/{finance → news}/gdelt.agency +5 -5
- package/stdlib/data/{finance → news}/gdelt.js +48 -63
- package/stdlib/data/people/littlesis.agency +6 -20
- package/stdlib/data/people/littlesis.js +75 -246
- package/stdlib/data/social/bluesky.agency +291 -0
- package/stdlib/data/social/bluesky.js +2974 -0
- package/stdlib/data/tech/hackernews.agency +12 -42
- package/stdlib/data/tech/hackernews.js +115 -591
- package/stdlib/data/tech/yc.agency +6 -20
- package/stdlib/data/tech/yc.js +102 -320
- package/stdlib/data/usaspending.agency +3 -17
- package/stdlib/data/usaspending.js +41 -205
- package/stdlib/data/wikidata.agency +4 -18
- package/stdlib/data/wikidata.js +66 -245
- package/stdlib/docs/cli/local.md +14 -11
- package/stdlib/docs/guide/using-local-models.md +188 -0
- package/stdlib/docs/stdlib/aws/s3.md +327 -0
- package/stdlib/docs/stdlib/capabilities.md +28 -15
- package/stdlib/docs/stdlib/data/connector.md +139 -0
- package/stdlib/docs/stdlib/data/finance/dbnomics.md +2 -2
- package/stdlib/docs/stdlib/data/finance/edgar.md +3 -3
- package/stdlib/docs/stdlib/data/finance/fred.md +5 -5
- package/stdlib/docs/stdlib/data/{finance → news}/gdelt.md +4 -4
- package/stdlib/docs/stdlib/data/people/littlesis.md +5 -5
- package/stdlib/docs/stdlib/data/social/bluesky.md +206 -0
- package/stdlib/docs/stdlib/data/tech/hackernews.md +4 -4
- package/stdlib/docs/stdlib/data/tech/yc.md +5 -5
- package/stdlib/docs/stdlib/data/usaspending.md +2 -2
- package/stdlib/docs/stdlib/data/wikidata.md +3 -3
- package/stdlib/docs/stdlib/image.md +26 -5
- package/stdlib/docs/stdlib/messaging/email.md +4 -4
- package/stdlib/docs/stdlib/speech.md +107 -20
- package/stdlib/docs/stdlib/tag.md +9 -5
- package/stdlib/docs/stdlib/thread.md +68 -22
- package/stdlib/image.agency +19 -3
- package/stdlib/image.js +7 -3
- package/stdlib/messaging/email.agency +104 -49
- package/stdlib/messaging/email.js +1 -1
- package/stdlib/speech.agency +143 -25
- package/stdlib/speech.js +432 -32
- package/stdlib/tag.agency +8 -4
- package/stdlib/tag.js +51 -8
- package/stdlib/thread.agency +35 -1
- package/stdlib/thread.js +171 -3
- package/dist/lib/stdlib/providers/llama-cpp.mjs +0 -79
- package/dist/lib/stdlib/providers/llamaModelConfig.d.ts +0 -7
- package/dist/lib/stdlib/providers/llamaModelConfig.js +0 -7
package/dist/lib/cli/local.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import { type RefreshResult } from "../stdlib/localModels.js";
|
|
2
|
-
export declare function aliasList(file?: string):
|
|
1
|
+
import { type ModelNameEntry, type RefreshResult } from "../stdlib/localModels.js";
|
|
2
|
+
export declare function aliasList(file?: string): ModelNameEntry[];
|
|
3
3
|
export declare function aliasAdd(name: string, uri: string, file?: string): string;
|
|
4
4
|
export declare function aliasRemove(name: string, file?: string): string;
|
|
5
|
+
/** Deliberately ungated: browsing the catalog needs no provider package
|
|
6
|
+
* (only download/remove do), and the pre-install experience — see what is
|
|
7
|
+
* available, then get told what to install — is the point. */
|
|
5
8
|
export declare function runList(): void;
|
|
6
|
-
export declare
|
|
9
|
+
export declare const CUSTOM_CHOICE = "__custom__";
|
|
10
|
+
/** Picker rows for the no-argument `agency local download`. Metadata-less
|
|
11
|
+
* aliases get a bare name; the trailing choice lets the user type an hf: URI
|
|
12
|
+
* or .gguf path. */
|
|
13
|
+
export declare function downloadChoices(entries: ModelNameEntry[]): {
|
|
14
|
+
title: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}[];
|
|
17
|
+
export declare function runDownload(value?: string): Promise<void>;
|
|
7
18
|
export declare function runRemove(name: string): void;
|
|
8
19
|
export declare function runResolve(value: string): void;
|
|
9
20
|
export declare function runAliasList(): void;
|
package/dist/lib/cli/local.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import { _resolveModelName, _downloadModel, _listDownloadedModels, _listModelNames, _modelsCacheDir, _aliasModel, _unaliasModel, _removeModel, hasLocalModelSupport, formatGB, formatModelCatalog, formatLocalList, _refreshCatalog, } from "../stdlib/localModels.js";
|
|
3
|
+
import { readDownloadManifest } from "../stdlib/localModelManifest.js";
|
|
2
4
|
import { ttyColor } from "../utils/termcolors.js";
|
|
3
5
|
/** Install-gate for I/O commands. Honors the AGENCY_LLAMA_PROVIDER_MODULE
|
|
4
6
|
* override the same way `requireSupport()` in `localModels.ts` does — a
|
|
@@ -31,26 +33,72 @@ export function aliasRemove(name, file) {
|
|
|
31
33
|
}
|
|
32
34
|
return inspected;
|
|
33
35
|
}
|
|
36
|
+
/** Deliberately ungated: browsing the catalog needs no provider package
|
|
37
|
+
* (only download/remove do), and the pre-install experience — see what is
|
|
38
|
+
* available, then get told what to install — is the point. */
|
|
34
39
|
export function runList() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const dir = _modelsCacheDir();
|
|
41
|
+
console.log(formatLocalList({
|
|
42
|
+
dir,
|
|
43
|
+
entries: _listModelNames(),
|
|
44
|
+
manifest: readDownloadManifest(dir),
|
|
45
|
+
files: _listDownloadedModels(),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
export const CUSTOM_CHOICE = "__custom__";
|
|
49
|
+
/** Picker rows for the no-argument `agency local download`. Metadata-less
|
|
50
|
+
* aliases get a bare name; the trailing choice lets the user type an hf: URI
|
|
51
|
+
* or .gguf path. */
|
|
52
|
+
export function downloadChoices(entries) {
|
|
53
|
+
const rows = entries.map((e) => ({
|
|
54
|
+
title: e.params !== undefined && e.sizeBytes !== undefined
|
|
55
|
+
? `${e.name} (${e.params}, ${formatGB(e.sizeBytes)})`
|
|
56
|
+
: e.name,
|
|
57
|
+
value: e.name,
|
|
58
|
+
}));
|
|
59
|
+
return [...rows, { title: "custom (hf: URI or .gguf path)…", value: CUSTOM_CHOICE }];
|
|
46
60
|
}
|
|
47
61
|
export async function runDownload(value) {
|
|
48
62
|
gate();
|
|
63
|
+
let picked = value;
|
|
64
|
+
if (picked === undefined) {
|
|
65
|
+
// Prompting needs BOTH ends of the terminal: a TTY stdout to draw on and
|
|
66
|
+
// a TTY stdin to read from (`agency local download < /dev/null` from a
|
|
67
|
+
// terminal has a TTY stdout but nothing to read).
|
|
68
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
69
|
+
// A script that reaches this point asked for a download and did not
|
|
70
|
+
// get one — print what is available and fail.
|
|
71
|
+
console.log(formatModelCatalog());
|
|
72
|
+
console.error("Pass a model: agency local download <name>");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const answer = await prompts({
|
|
76
|
+
type: "select",
|
|
77
|
+
name: "model",
|
|
78
|
+
message: "Which model do you want to download?",
|
|
79
|
+
choices: downloadChoices(_listModelNames()),
|
|
80
|
+
});
|
|
81
|
+
// Cancellation can surface as a missing key or as null — treat both as
|
|
82
|
+
// "exit 0, nothing downloaded".
|
|
83
|
+
if (answer.model == null)
|
|
84
|
+
return;
|
|
85
|
+
picked = answer.model;
|
|
86
|
+
if (picked === CUSTOM_CHOICE) {
|
|
87
|
+
const custom = await prompts({
|
|
88
|
+
type: "text",
|
|
89
|
+
name: "value",
|
|
90
|
+
message: "hf: URI or .gguf path:",
|
|
91
|
+
});
|
|
92
|
+
if (custom.value == null || custom.value === "")
|
|
93
|
+
return;
|
|
94
|
+
picked = custom.value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
49
97
|
// Show the source it resolved to (the hf: URI for a name/alias) and the
|
|
50
98
|
// local path it landed at. For a .gguf-path input the two are the same, so
|
|
51
99
|
// the source line is skipped.
|
|
52
|
-
const source = _resolveModelName(
|
|
53
|
-
const modelPath = await _downloadModel(
|
|
100
|
+
const source = _resolveModelName(picked);
|
|
101
|
+
const modelPath = await _downloadModel(picked);
|
|
54
102
|
if (source !== modelPath) {
|
|
55
103
|
console.log(`source: ${source}`);
|
|
56
104
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ResolvedModelFlag } from "../config.js";
|
|
2
|
+
/** Turn `agency run --local <value>` into the shared model-flag shape:
|
|
3
|
+
* resolve the name (curated / alias / hf: URI / .gguf path), download and
|
|
4
|
+
* verify if needed (progress prints here, in the parent, before the program
|
|
5
|
+
* starts), and pin the llama-cpp provider. Errors (package missing, unknown
|
|
6
|
+
* name, failed download) carry user-ready messages from localModels.
|
|
7
|
+
*
|
|
8
|
+
* The path is absolutized before it is baked into config: LlamaCPP rejects a
|
|
9
|
+
* bare separator-less filename (ambiguous with a model name), which is what
|
|
10
|
+
* a user-supplied `--local model.gguf` would otherwise arrive as, and an
|
|
11
|
+
* absolute path also keeps the child process independent of cwd drift. */
|
|
12
|
+
export declare function resolveLocalRunFlag(value: string): Promise<ResolvedModelFlag>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { _registerLocalModel } from "../stdlib/localModels.js";
|
|
3
|
+
/** Turn `agency run --local <value>` into the shared model-flag shape:
|
|
4
|
+
* resolve the name (curated / alias / hf: URI / .gguf path), download and
|
|
5
|
+
* verify if needed (progress prints here, in the parent, before the program
|
|
6
|
+
* starts), and pin the llama-cpp provider. Errors (package missing, unknown
|
|
7
|
+
* name, failed download) carry user-ready messages from localModels.
|
|
8
|
+
*
|
|
9
|
+
* The path is absolutized before it is baked into config: LlamaCPP rejects a
|
|
10
|
+
* bare separator-less filename (ambiguous with a model name), which is what
|
|
11
|
+
* a user-supplied `--local model.gguf` would otherwise arrive as, and an
|
|
12
|
+
* absolute path also keeps the child process independent of cwd drift. */
|
|
13
|
+
export async function resolveLocalRunFlag(value) {
|
|
14
|
+
const modelPath = await _registerLocalModel(value);
|
|
15
|
+
return { model: path.resolve(modelPath), explicitProvider: "llama-cpp" };
|
|
16
|
+
}
|
|
@@ -5,4 +5,8 @@ export type RemoteDeployOptions = {
|
|
|
5
5
|
apiKeyEnv?: string;
|
|
6
6
|
dryRun?: boolean;
|
|
7
7
|
};
|
|
8
|
-
|
|
8
|
+
/** What actually happened, for callers that gate follow-up work on a real
|
|
9
|
+
* upload (schedule add's deploy-if-missing). A deploy error never produces an
|
|
10
|
+
* outcome — that path exits the process. */
|
|
11
|
+
export type RunDeployOutcome = "deployed" | "aborted" | "preview";
|
|
12
|
+
export declare function runDeploy(file: string, options: RemoteDeployOptions, context: RemoteCommandContext): Promise<RunDeployOutcome>;
|
|
@@ -17,7 +17,7 @@ export async function runDeploy(file, options, context) {
|
|
|
17
17
|
color.dim("Nodes and functions must be marked 'export' to be served."));
|
|
18
18
|
if (!(await confirmDeployWithoutExports({ dryRun: options.dryRun }))) {
|
|
19
19
|
console.log("Aborted.");
|
|
20
|
-
return;
|
|
20
|
+
return "aborted";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
const outcome = await deploy(file, context.config, options);
|
|
@@ -26,16 +26,17 @@ export async function runDeploy(file, options, context) {
|
|
|
26
26
|
process.exit(1);
|
|
27
27
|
}
|
|
28
28
|
if (outcome.kind !== "deployed") {
|
|
29
|
-
return; // a --dry-run preview never writes a binding
|
|
29
|
+
return "preview"; // a --dry-run preview never writes a binding
|
|
30
30
|
}
|
|
31
31
|
const base = serveBaseUrl(outcome.endpointUrls);
|
|
32
32
|
const address = base ? parseServeBaseUrl(base) : null;
|
|
33
33
|
if (!address) {
|
|
34
34
|
console.log(color.yellow("Deployed, but could not derive a serve URL to link."));
|
|
35
|
-
return;
|
|
35
|
+
return "deployed";
|
|
36
36
|
}
|
|
37
37
|
writeBinding(context.configPath, address);
|
|
38
38
|
console.log(`\n${color.green("Linked")} this directory to ${color.bold(address.filename)}.`);
|
|
39
|
+
return "deployed";
|
|
39
40
|
}
|
|
40
41
|
function tryCountExports(file, config) {
|
|
41
42
|
try {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ProjectCommandOptions, RemoteCommandContext } from "./util.js";
|
|
2
|
+
import { SecretRequestError } from "../../statelog/secretsClient.js";
|
|
3
|
+
import type { SecretValueSources } from "../secretsInput.js";
|
|
4
|
+
/** The set recipe's terminal dependencies, injected by the Commander action
|
|
5
|
+
* and faked in tests. Same fields as SecretValueSources minus fromEnv, which
|
|
6
|
+
* arrives via options. */
|
|
7
|
+
export type SecretsSetIO = Omit<SecretValueSources, "fromEnv">;
|
|
8
|
+
export type SecretsSetResult = {
|
|
9
|
+
kind: "set";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "canceled";
|
|
12
|
+
};
|
|
13
|
+
/** THE one presenter for SecretRequestError, used by all four recipes
|
|
14
|
+
* (including every collected import failure): redacts any additional known
|
|
15
|
+
* values BEFORE terminal-safe escaping, then appends the trusted one-line
|
|
16
|
+
* full-access-key guidance on status 401/403. Stays one line so import
|
|
17
|
+
* summaries keep one outcome per line. */
|
|
18
|
+
export declare function presentSecretError(error: SecretRequestError, sensitiveValues?: string[]): string;
|
|
19
|
+
export declare function runSecretsSet(name: string, options: ProjectCommandOptions & {
|
|
20
|
+
fromEnv?: string;
|
|
21
|
+
}, context: RemoteCommandContext, io: SecretsSetIO): Promise<SecretsSetResult>;
|
|
22
|
+
export declare function runSecretsList(options: ProjectCommandOptions, context: RemoteCommandContext): Promise<void>;
|
|
23
|
+
export declare function runSecretsRm(name: string, options: ProjectCommandOptions, context: RemoteCommandContext): Promise<void>;
|
|
24
|
+
export type ImportResult = {
|
|
25
|
+
kind: "declined";
|
|
26
|
+
} | {
|
|
27
|
+
kind: "succeeded";
|
|
28
|
+
} | {
|
|
29
|
+
kind: "failed";
|
|
30
|
+
};
|
|
31
|
+
export type SecretsImportIO = {
|
|
32
|
+
stdinIsTty: boolean;
|
|
33
|
+
readStdin: () => Promise<string>;
|
|
34
|
+
confirm: (question: string) => Promise<boolean>;
|
|
35
|
+
};
|
|
36
|
+
export declare function runSecretsImport(file: string | undefined, options: ProjectCommandOptions, context: RemoteCommandContext, io: SecretsImportIO): Promise<ImportResult>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// `agency remote secrets` — thin recipes over the sealed secrets client. The
|
|
2
|
+
// store is write-only (no route returns a value), and these recipes uphold the
|
|
3
|
+
// CLI's matching invariants: a value reaches `set` only through injected seams
|
|
4
|
+
// (never argv), and no code path prints one — presentSecretError is the single
|
|
5
|
+
// place that composes redaction, terminal-safe escaping, and trusted auth
|
|
6
|
+
// guidance, in that order. Recipes render and return semantic outcomes; the
|
|
7
|
+
// Commander actions own process.exitCode.
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import { color } from "../../../utils/termcolors.js";
|
|
11
|
+
import { resolveProjectTarget, fail } from "./util.js";
|
|
12
|
+
import { createSecretsClient, SecretRequestError } from "../../statelog/secretsClient.js";
|
|
13
|
+
import { redactValues } from "../../statelog/redact.js";
|
|
14
|
+
import { resolveSecretValue, parseEnvSource, terminalSafe } from "../secretsInput.js";
|
|
15
|
+
/** THE one presenter for SecretRequestError, used by all four recipes
|
|
16
|
+
* (including every collected import failure): redacts any additional known
|
|
17
|
+
* values BEFORE terminal-safe escaping, then appends the trusted one-line
|
|
18
|
+
* full-access-key guidance on status 401/403. Stays one line so import
|
|
19
|
+
* summaries keep one outcome per line. */
|
|
20
|
+
export function presentSecretError(error, sensitiveValues = []) {
|
|
21
|
+
const safe = terminalSafe(redactValues(error.message, sensitiveValues));
|
|
22
|
+
if (error.status === 401 || error.status === 403) {
|
|
23
|
+
return `${safe} — Secret management needs a full-access API key; invoke-only keys and sessions are rejected.`;
|
|
24
|
+
}
|
|
25
|
+
return safe;
|
|
26
|
+
}
|
|
27
|
+
export async function runSecretsSet(name, options, context, io) {
|
|
28
|
+
const target = resolveProjectTarget(context, options);
|
|
29
|
+
const resolved = await resolveSecretValue(name, { ...io, fromEnv: options.fromEnv });
|
|
30
|
+
if (resolved.kind === "canceled") {
|
|
31
|
+
console.log("Canceled.");
|
|
32
|
+
return { kind: "canceled" };
|
|
33
|
+
}
|
|
34
|
+
if (resolved.kind === "error") {
|
|
35
|
+
fail(resolved.message);
|
|
36
|
+
}
|
|
37
|
+
const client = createSecretsClient(target.origin, target.projectSlug, target.apiKey);
|
|
38
|
+
try {
|
|
39
|
+
await client.set(name, resolved.value);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
failSecretRequest(error);
|
|
43
|
+
}
|
|
44
|
+
console.log(`${color.green("Set")} secret ${terminalSafe(name)}.`);
|
|
45
|
+
// The hint shows real call-site syntax, so the name renders as an actual
|
|
46
|
+
// string literal — JSON.stringify quotes AND escapes it in one step (a
|
|
47
|
+
// terminalSafe'd name would end up double-quoted here).
|
|
48
|
+
console.log(`Available to hosted runs as env(${JSON.stringify(name)}) from the next invocation.`);
|
|
49
|
+
return { kind: "set" };
|
|
50
|
+
}
|
|
51
|
+
export async function runSecretsList(options, context) {
|
|
52
|
+
const target = resolveProjectTarget(context, options);
|
|
53
|
+
const client = createSecretsClient(target.origin, target.projectSlug, target.apiKey);
|
|
54
|
+
let secrets;
|
|
55
|
+
try {
|
|
56
|
+
secrets = await client.list();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
failSecretRequest(error);
|
|
60
|
+
}
|
|
61
|
+
console.log(formatSecretsTable(secrets));
|
|
62
|
+
}
|
|
63
|
+
export async function runSecretsRm(name, options, context) {
|
|
64
|
+
const target = resolveProjectTarget(context, options);
|
|
65
|
+
const client = createSecretsClient(target.origin, target.projectSlug, target.apiKey);
|
|
66
|
+
try {
|
|
67
|
+
await client.delete(name);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
failSecretRequest(error);
|
|
71
|
+
}
|
|
72
|
+
console.log(`${color.green("Removed")} secret ${terminalSafe(name)}.`);
|
|
73
|
+
}
|
|
74
|
+
export async function runSecretsImport(file, options, context, io) {
|
|
75
|
+
const target = resolveProjectTarget(context, options);
|
|
76
|
+
const fromStdin = file === "-";
|
|
77
|
+
const sourceName = fromStdin ? "<stdin>" : (file ?? ".env");
|
|
78
|
+
// The file argument is user/argv-controlled display text like any other.
|
|
79
|
+
const safeSource = terminalSafe(sourceName);
|
|
80
|
+
let text;
|
|
81
|
+
if (fromStdin) {
|
|
82
|
+
text = await io.readStdin();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const sourcePath = path.resolve(process.cwd(), sourceName);
|
|
86
|
+
try {
|
|
87
|
+
text = fs.readFileSync(sourcePath, "utf-8");
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
fail(`Could not read ${safeSource}: ${errorMessage(error)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const { entries } = parseEnvSource(text);
|
|
94
|
+
if (entries.length === 0) {
|
|
95
|
+
fail(`No variables found in ${safeSource}.`);
|
|
96
|
+
}
|
|
97
|
+
// Confirmation is for FILE sources on a TTY only. An explicit `-` never
|
|
98
|
+
// prompts: after an interactive stdin entry the stream is exhausted at EOF,
|
|
99
|
+
// and choosing `-` is itself the authorization.
|
|
100
|
+
if (!fromStdin && io.stdinIsTty) {
|
|
101
|
+
const plural = entries.length === 1 ? "" : "s";
|
|
102
|
+
console.log(`Import ${entries.length} secret${plural} into project ${target.projectSlug} from ${safeSource}:`);
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
console.log(` ${terminalSafe(entry.name)}`);
|
|
105
|
+
}
|
|
106
|
+
if (!(await io.confirm("Continue?"))) {
|
|
107
|
+
console.log("Canceled.");
|
|
108
|
+
return { kind: "declined" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const allValues = entries.map((entry) => entry.value);
|
|
112
|
+
const client = createSecretsClient(target.origin, target.projectSlug, target.apiKey);
|
|
113
|
+
const outcomes = [];
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (entry.value === "") {
|
|
116
|
+
outcomes.push({ name: entry.name, ok: false, message: "empty value — nothing sent" });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
// The whole batch joins the FIRST redaction pass: a response echoing a
|
|
121
|
+
// DIFFERENT import's value that overlaps this one would otherwise be
|
|
122
|
+
// partially destroyed before the presenter's pass could match it.
|
|
123
|
+
await client.set(entry.name, entry.value, { alsoRedact: allValues });
|
|
124
|
+
outcomes.push({ name: entry.name, ok: true });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (!(error instanceof SecretRequestError)) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
outcomes.push({
|
|
131
|
+
name: entry.name,
|
|
132
|
+
ok: false,
|
|
133
|
+
message: presentSecretError(error, allValues),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
renderImportSummary(outcomes);
|
|
138
|
+
return outcomes.some((outcome) => !outcome.ok) ? { kind: "failed" } : { kind: "succeeded" };
|
|
139
|
+
}
|
|
140
|
+
function renderImportSummary(outcomes) {
|
|
141
|
+
const failed = outcomes.filter((outcome) => !outcome.ok);
|
|
142
|
+
const setCount = outcomes.length - failed.length;
|
|
143
|
+
if (failed.length === 0) {
|
|
144
|
+
const plural = setCount === 1 ? "" : "s";
|
|
145
|
+
console.log(`${color.green("Imported")} ${setCount} secret${plural}.`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(`Imported ${setCount} of ${outcomes.length}; ${failed.length} failed:`);
|
|
149
|
+
for (const outcome of failed) {
|
|
150
|
+
console.log(` ${terminalSafe(outcome.name)}: ${outcome.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function errorMessage(error) {
|
|
154
|
+
return error instanceof Error ? error.message : String(error);
|
|
155
|
+
}
|
|
156
|
+
function failSecretRequest(error) {
|
|
157
|
+
if (!(error instanceof SecretRequestError)) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
fail(presentSecretError(error));
|
|
161
|
+
}
|
|
162
|
+
function formatSecretsTable(secrets) {
|
|
163
|
+
if (secrets.length === 0) {
|
|
164
|
+
return "No secrets set for this project. Use 'agency remote secrets set <NAME>' to add one.";
|
|
165
|
+
}
|
|
166
|
+
const rows = secrets.map((secret) => ({
|
|
167
|
+
name: terminalSafe(secret.name),
|
|
168
|
+
created: secret.createdAt.slice(0, 10),
|
|
169
|
+
updated: secret.updatedAt.slice(0, 10),
|
|
170
|
+
}));
|
|
171
|
+
const headers = { name: "Name", created: "Created", updated: "Updated" };
|
|
172
|
+
const columns = ["name", "created", "updated"];
|
|
173
|
+
const width = (column) => Math.max(headers[column].length, ...rows.map((row) => row[column].length)) + 2;
|
|
174
|
+
const render = (row) => columns
|
|
175
|
+
.map((column, index) => index === columns.length - 1 ? row[column] : row[column].padEnd(width(column)))
|
|
176
|
+
.join("");
|
|
177
|
+
return [render(headers), ...rows.map(render)].join("\n");
|
|
178
|
+
}
|
|
@@ -3,4 +3,13 @@ export type ConfirmDeployOptions = {
|
|
|
3
3
|
isTty?: boolean;
|
|
4
4
|
prompt?: (question: string) => Promise<boolean>;
|
|
5
5
|
};
|
|
6
|
+
/** The deploy policy: a dry-run and a non-TTY run bypass the prompt and
|
|
7
|
+
* proceed — only an interactive TTY asks. */
|
|
6
8
|
export declare function confirmDeployWithoutExports(options?: ConfirmDeployOptions): Promise<boolean>;
|
|
9
|
+
/** The raw readline yes/no lifecycle, policy-free. */
|
|
10
|
+
export declare function confirmQuestion(question: string): Promise<boolean>;
|
|
11
|
+
/** Hidden-input prompt for a secret value. Resolves undefined on cancellation
|
|
12
|
+
* (ctrl-C / EOF) — `prompts` omits the answer key then — and preserves an
|
|
13
|
+
* empty entry as "" so the caller can distinguish "typed nothing" from
|
|
14
|
+
* "canceled". The name is untrusted display text and rendered terminal-safe. */
|
|
15
|
+
export declare function promptSecretValue(name: string): Promise<string | undefined>;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The remote commands' concrete terminal-prompt adapters: the raw yes/no
|
|
2
|
+
// readline lifecycle, the hidden secret-value prompt, and the deploy-specific
|
|
3
|
+
// "no callable endpoints?" gate built on top. This module owns terminal
|
|
4
|
+
// mechanics only — each command keeps its own policy (when to ask, what a
|
|
5
|
+
// decline means).
|
|
5
6
|
import * as readline from "node:readline/promises";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
import { terminalSafe } from "./secretsInput.js";
|
|
9
|
+
/** The deploy policy: a dry-run and a non-TTY run bypass the prompt and
|
|
10
|
+
* proceed — only an interactive TTY asks. */
|
|
6
11
|
export async function confirmDeployWithoutExports(options = {}) {
|
|
7
12
|
if (options.dryRun) {
|
|
8
13
|
return true;
|
|
@@ -11,10 +16,11 @@ export async function confirmDeployWithoutExports(options = {}) {
|
|
|
11
16
|
if (!isTty) {
|
|
12
17
|
return true;
|
|
13
18
|
}
|
|
14
|
-
const prompt = options.prompt ??
|
|
19
|
+
const prompt = options.prompt ?? confirmQuestion;
|
|
15
20
|
return prompt("Upload anyway?");
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
/** The raw readline yes/no lifecycle, policy-free. */
|
|
23
|
+
export async function confirmQuestion(question) {
|
|
18
24
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
19
25
|
try {
|
|
20
26
|
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
@@ -24,3 +30,15 @@ async function defaultPrompt(question) {
|
|
|
24
30
|
rl.close();
|
|
25
31
|
}
|
|
26
32
|
}
|
|
33
|
+
/** Hidden-input prompt for a secret value. Resolves undefined on cancellation
|
|
34
|
+
* (ctrl-C / EOF) — `prompts` omits the answer key then — and preserves an
|
|
35
|
+
* empty entry as "" so the caller can distinguish "typed nothing" from
|
|
36
|
+
* "canceled". The name is untrusted display text and rendered terminal-safe. */
|
|
37
|
+
export async function promptSecretValue(name) {
|
|
38
|
+
const response = await prompts({
|
|
39
|
+
type: "invisible",
|
|
40
|
+
name: "value",
|
|
41
|
+
message: `Enter value for ${terminalSafe(name)}:`,
|
|
42
|
+
});
|
|
43
|
+
return typeof response.value === "string" ? response.value : undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type SecretValueSources = {
|
|
2
|
+
/** --from-env VAR: the NAME of a local environment variable to copy. */
|
|
3
|
+
fromEnv?: string;
|
|
4
|
+
stdinIsTty: boolean;
|
|
5
|
+
readStdin: () => Promise<string>;
|
|
6
|
+
/** Hidden prompt; resolves undefined when the user cancels (ctrl-C / EOF). */
|
|
7
|
+
promptHidden: (name: string) => Promise<string | undefined>;
|
|
8
|
+
env: NodeJS.ProcessEnv;
|
|
9
|
+
};
|
|
10
|
+
export type SecretValueResult = {
|
|
11
|
+
kind: "value";
|
|
12
|
+
value: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "canceled";
|
|
15
|
+
} | {
|
|
16
|
+
kind: "error";
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
/** Where a secret's value comes from, in precedence order: --from-env wins;
|
|
20
|
+
* else piped stdin; else the hidden TTY prompt. Never argv. */
|
|
21
|
+
export declare function resolveSecretValue(name: string, sources: SecretValueSources): Promise<SecretValueResult>;
|
|
22
|
+
/** One terminal "\r\n" or "\n" removed — exactly one, so a value meant to keep
|
|
23
|
+
* a final newline supplies two (printf 'value\n\n'). Interior newlines are
|
|
24
|
+
* untouched. */
|
|
25
|
+
export declare function stripOneTrailingNewline(text: string): string;
|
|
26
|
+
export type ParsedEnvSource = {
|
|
27
|
+
/** First-insertion key order; on a duplicate key the LAST assignment's value
|
|
28
|
+
* wins (in the original position). No duplicate reporting — a `B=` line
|
|
29
|
+
* inside a multiline quoted value is NOT an assignment, and
|
|
30
|
+
* last-value-wins is the behavior that matters. */
|
|
31
|
+
entries: {
|
|
32
|
+
name: string;
|
|
33
|
+
value: string;
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
/** A small, OWNED dotenv parser. Deliberately not node:util's parseEnv: its
|
|
37
|
+
* quoted-value handling differs across Node releases inside the supported
|
|
38
|
+
* engine range (22.13.0 mis-splits `B="x=y"` into a bogus extra entry), and
|
|
39
|
+
* secret values must parse identically on every supported version. Semantics
|
|
40
|
+
* (pinned by tests): `NAME=value` lines; optional `export ` prefix; `#`
|
|
41
|
+
* comments and blank lines skipped; single- or double-quoted values may span
|
|
42
|
+
* lines and keep `=`/`#` literally; quoted content is LITERAL (no escape
|
|
43
|
+
* expansion); unquoted values run to end of line, trimmed. Never touches
|
|
44
|
+
* process.env. */
|
|
45
|
+
export declare function parseEnvSource(text: string): ParsedEnvSource;
|
|
46
|
+
/** Single-line, terminal-safe rendering for untrusted display text (names,
|
|
47
|
+
* server messages): a string containing terminal-control characters is
|
|
48
|
+
* JSON-quoted so it cannot manipulate the terminal; plain strings pass
|
|
49
|
+
* through unchanged. "Control" covers C0, DEL, and the C1 range (U+009B is a
|
|
50
|
+
* bare CSI) plus the U+2028/U+2029 line separators — and because
|
|
51
|
+
* JSON.stringify escapes only C0, the quoted form is post-processed so every
|
|
52
|
+
* such character ends up as a \\uXXXX escape. Always applied AFTER
|
|
53
|
+
* redaction. */
|
|
54
|
+
export declare function terminalSafe(text: string): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Pure input handling for the remote secrets commands — a sibling of args.ts.
|
|
2
|
+
// No terminal, no `prompts`, no process globals: every environmental input
|
|
3
|
+
// (TTY-ness, stdin, the hidden prompt, the env) arrives injected, so the
|
|
4
|
+
// precedence rules and parsing are testable without a terminal. The concrete
|
|
5
|
+
// adapters live in confirmation.ts.
|
|
6
|
+
/** Where a secret's value comes from, in precedence order: --from-env wins;
|
|
7
|
+
* else piped stdin; else the hidden TTY prompt. Never argv. */
|
|
8
|
+
export async function resolveSecretValue(name, sources) {
|
|
9
|
+
if (sources.fromEnv !== undefined) {
|
|
10
|
+
const value = sources.env[sources.fromEnv];
|
|
11
|
+
if (value === undefined || value === "") {
|
|
12
|
+
return {
|
|
13
|
+
kind: "error",
|
|
14
|
+
message: `$${sources.fromEnv} is not set (or is empty) — --from-env needs a non-empty variable.`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return { kind: "value", value };
|
|
18
|
+
}
|
|
19
|
+
if (!sources.stdinIsTty) {
|
|
20
|
+
const value = stripOneTrailingNewline(await sources.readStdin());
|
|
21
|
+
if (value === "") {
|
|
22
|
+
return {
|
|
23
|
+
kind: "error",
|
|
24
|
+
message: "No value on stdin — pipe a non-empty value, or run interactively for a hidden prompt.",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { kind: "value", value };
|
|
28
|
+
}
|
|
29
|
+
const entered = await sources.promptHidden(name);
|
|
30
|
+
if (entered === undefined) {
|
|
31
|
+
return { kind: "canceled" };
|
|
32
|
+
}
|
|
33
|
+
if (entered === "") {
|
|
34
|
+
return { kind: "error", message: "Secret value must not be empty." };
|
|
35
|
+
}
|
|
36
|
+
return { kind: "value", value: entered };
|
|
37
|
+
}
|
|
38
|
+
/** One terminal "\r\n" or "\n" removed — exactly one, so a value meant to keep
|
|
39
|
+
* a final newline supplies two (printf 'value\n\n'). Interior newlines are
|
|
40
|
+
* untouched. */
|
|
41
|
+
export function stripOneTrailingNewline(text) {
|
|
42
|
+
if (text.endsWith("\r\n")) {
|
|
43
|
+
return text.slice(0, -2);
|
|
44
|
+
}
|
|
45
|
+
if (text.endsWith("\n")) {
|
|
46
|
+
return text.slice(0, -1);
|
|
47
|
+
}
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
const NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
51
|
+
/** A small, OWNED dotenv parser. Deliberately not node:util's parseEnv: its
|
|
52
|
+
* quoted-value handling differs across Node releases inside the supported
|
|
53
|
+
* engine range (22.13.0 mis-splits `B="x=y"` into a bogus extra entry), and
|
|
54
|
+
* secret values must parse identically on every supported version. Semantics
|
|
55
|
+
* (pinned by tests): `NAME=value` lines; optional `export ` prefix; `#`
|
|
56
|
+
* comments and blank lines skipped; single- or double-quoted values may span
|
|
57
|
+
* lines and keep `=`/`#` literally; quoted content is LITERAL (no escape
|
|
58
|
+
* expansion); unquoted values run to end of line, trimmed. Never touches
|
|
59
|
+
* process.env. */
|
|
60
|
+
export function parseEnvSource(text) {
|
|
61
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
62
|
+
const values = new Map();
|
|
63
|
+
for (let index = 0; index < lines.length; index++) {
|
|
64
|
+
let line = lines[index].trim();
|
|
65
|
+
if (line === "" || line.startsWith("#")) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (line.startsWith("export ")) {
|
|
69
|
+
line = line.slice("export ".length).trim();
|
|
70
|
+
}
|
|
71
|
+
const equals = line.indexOf("=");
|
|
72
|
+
if (equals <= 0) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const name = line.slice(0, equals).trim();
|
|
76
|
+
if (!NAME_PATTERN.test(name)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const rest = line.slice(equals + 1).trim();
|
|
80
|
+
if (rest.startsWith('"') || rest.startsWith("'")) {
|
|
81
|
+
const quote = rest[0];
|
|
82
|
+
let accumulated = rest.slice(1);
|
|
83
|
+
let closing = accumulated.indexOf(quote);
|
|
84
|
+
while (closing === -1 && index + 1 < lines.length) {
|
|
85
|
+
index++;
|
|
86
|
+
accumulated += `\n${lines[index]}`;
|
|
87
|
+
closing = accumulated.indexOf(quote);
|
|
88
|
+
}
|
|
89
|
+
values.set(name, closing === -1 ? accumulated : accumulated.slice(0, closing));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
values.set(name, rest);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { entries: [...values.entries()].map(([name, value]) => ({ name, value })) };
|
|
96
|
+
}
|
|
97
|
+
/** Single-line, terminal-safe rendering for untrusted display text (names,
|
|
98
|
+
* server messages): a string containing terminal-control characters is
|
|
99
|
+
* JSON-quoted so it cannot manipulate the terminal; plain strings pass
|
|
100
|
+
* through unchanged. "Control" covers C0, DEL, and the C1 range (U+009B is a
|
|
101
|
+
* bare CSI) plus the U+2028/U+2029 line separators — and because
|
|
102
|
+
* JSON.stringify escapes only C0, the quoted form is post-processed so every
|
|
103
|
+
* such character ends up as a \\uXXXX escape. Always applied AFTER
|
|
104
|
+
* redaction. */
|
|
105
|
+
export function terminalSafe(text) {
|
|
106
|
+
if (![...text].some((character) => isTerminalUnsafe(character.codePointAt(0) ?? 0))) {
|
|
107
|
+
return text;
|
|
108
|
+
}
|
|
109
|
+
let escaped = "";
|
|
110
|
+
for (const character of JSON.stringify(text)) {
|
|
111
|
+
const code = character.codePointAt(0) ?? 0;
|
|
112
|
+
escaped += isTerminalUnsafe(code)
|
|
113
|
+
? `\\u${code.toString(16).padStart(4, "0")}`
|
|
114
|
+
: character;
|
|
115
|
+
}
|
|
116
|
+
return escaped;
|
|
117
|
+
}
|
|
118
|
+
function isTerminalUnsafe(code) {
|
|
119
|
+
return (code < 0x20 ||
|
|
120
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
121
|
+
code === 0x2028 ||
|
|
122
|
+
code === 0x2029);
|
|
123
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const PRESETS: Record<string, string>;
|
|
2
|
+
export declare function canonicalPreset(preset: string): string;
|
|
2
3
|
export declare function presetToCron(preset: string): string;
|
|
3
4
|
export declare function validateCron(expr: string): boolean;
|
|
4
5
|
export declare function resolveCron(opts: {
|