agency-lang 0.13.4 → 0.14.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/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/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/memory/manager.js +4 -2
- 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 +37 -0
- package/dist/lib/statelogClient.js +54 -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/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 +190 -15
- package/package.json +2 -2
- package/stdlib/aws/s3.agency +226 -0
- package/stdlib/aws/s3.js +1389 -0
- package/stdlib/capabilities.agency +4 -1
- package/stdlib/capabilities.js +3 -1
- package/stdlib/docs/stdlib/aws/s3.md +327 -0
- package/stdlib/docs/stdlib/capabilities.md +21 -10
- 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
|
@@ -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: {
|
|
@@ -8,8 +8,22 @@ export const PRESETS = {
|
|
|
8
8
|
weekly: "0 9 * * 1",
|
|
9
9
|
monthly: "0 9 1 * *",
|
|
10
10
|
};
|
|
11
|
+
// `--every hour` reads better than `--every hourly`, so each preset also
|
|
12
|
+
// accepts its singular noun. Aliases normalize to the canonical name before
|
|
13
|
+
// lookup, storage, and display.
|
|
14
|
+
const PRESET_ALIASES = {
|
|
15
|
+
hour: "hourly",
|
|
16
|
+
day: "daily",
|
|
17
|
+
weekday: "weekdays",
|
|
18
|
+
weekend: "weekends",
|
|
19
|
+
week: "weekly",
|
|
20
|
+
month: "monthly",
|
|
21
|
+
};
|
|
22
|
+
export function canonicalPreset(preset) {
|
|
23
|
+
return PRESET_ALIASES[preset] ?? preset;
|
|
24
|
+
}
|
|
11
25
|
export function presetToCron(preset) {
|
|
12
|
-
const cron = PRESETS[preset];
|
|
26
|
+
const cron = PRESETS[canonicalPreset(preset)];
|
|
13
27
|
if (!cron) {
|
|
14
28
|
throw new Error(`Unknown preset "${preset}". Valid presets: ${Object.keys(PRESETS).join(", ")}`);
|
|
15
29
|
}
|
|
@@ -48,7 +62,8 @@ export function validateCron(expr) {
|
|
|
48
62
|
// --- Resolution ---
|
|
49
63
|
export function resolveCron(opts) {
|
|
50
64
|
if (opts.every) {
|
|
51
|
-
|
|
65
|
+
const preset = canonicalPreset(opts.every);
|
|
66
|
+
return { cron: presetToCron(preset), preset };
|
|
52
67
|
}
|
|
53
68
|
if (opts.cron) {
|
|
54
69
|
if (!validateCron(opts.cron)) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ProjectCommandOptions, RemoteCommandContext } from "../remote/commands/util.js";
|
|
2
|
+
import type { RemoteArgsOptions } from "../remote/args.js";
|
|
3
|
+
import type { CreateScheduleInput, PatchScheduleInput } from "../statelog/schedulesClient.js";
|
|
4
|
+
export type DeployMode = "if-missing" | "always" | "never";
|
|
5
|
+
export type AddRemoteOptions = ProjectCommandOptions & RemoteArgsOptions & {
|
|
6
|
+
node?: string;
|
|
7
|
+
function?: string;
|
|
8
|
+
every?: string;
|
|
9
|
+
cron?: string;
|
|
10
|
+
timezone?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
redeploy?: boolean;
|
|
13
|
+
/** Commander's `--no-deploy` value: `false` when the flag was passed. */
|
|
14
|
+
deploy?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type ResolvedScheduleAdd = {
|
|
17
|
+
input: CreateScheduleInput;
|
|
18
|
+
deployMode: DeployMode;
|
|
19
|
+
};
|
|
20
|
+
/** Translate `schedule add --backend remote` options into a create request and
|
|
21
|
+
* a deployment policy. Pure: every invalid combination throws before any
|
|
22
|
+
* target resolution or network work can happen. */
|
|
23
|
+
export declare function resolveScheduleAdd(file: string, options: AddRemoteOptions, defaultTimezone: string): ResolvedScheduleAdd;
|
|
24
|
+
export declare function addRemote(file: string, options: AddRemoteOptions, context: RemoteCommandContext): Promise<void>;
|
|
25
|
+
export type EditRemoteOptions = ProjectCommandOptions & {
|
|
26
|
+
every?: string;
|
|
27
|
+
cron?: string;
|
|
28
|
+
timezone?: string;
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** Translate edit flags into the server's PATCH body. Only cadence, timezone,
|
|
33
|
+
* and enabled are patchable — target, name, and args are immutable on the
|
|
34
|
+
* server (delete and re-add to change them). Pure; throws on any invalid or
|
|
35
|
+
* empty combination. */
|
|
36
|
+
export declare function resolveSchedulePatch(options: EditRemoteOptions): PatchScheduleInput;
|
|
37
|
+
export declare function listRemote(options: ProjectCommandOptions, context: RemoteCommandContext): Promise<void>;
|
|
38
|
+
export declare function removeRemote(id: string, options: ProjectCommandOptions, context: RemoteCommandContext): Promise<void>;
|
|
39
|
+
export declare function editRemote(id: string, options: EditRemoteOptions, context: RemoteCommandContext): Promise<void>;
|