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.
Files changed (101) hide show
  1. package/dist/lib/cli/remote/commands/deploy.d.ts +5 -1
  2. package/dist/lib/cli/remote/commands/deploy.js +4 -3
  3. package/dist/lib/cli/remote/commands/secrets.d.ts +36 -0
  4. package/dist/lib/cli/remote/commands/secrets.js +178 -0
  5. package/dist/lib/cli/remote/confirmation.d.ts +9 -0
  6. package/dist/lib/cli/remote/confirmation.js +24 -6
  7. package/dist/lib/cli/remote/secretsInput.d.ts +54 -0
  8. package/dist/lib/cli/remote/secretsInput.js +123 -0
  9. package/dist/lib/cli/schedule/cron.d.ts +1 -0
  10. package/dist/lib/cli/schedule/cron.js +17 -2
  11. package/dist/lib/cli/schedule/remote.d.ts +39 -0
  12. package/dist/lib/cli/schedule/remote.js +242 -0
  13. package/dist/lib/cli/statelog/accountClient.js +41 -54
  14. package/dist/lib/cli/statelog/jsonBody.d.ts +20 -0
  15. package/dist/lib/cli/statelog/jsonBody.js +49 -0
  16. package/dist/lib/cli/statelog/projectClient.js +38 -62
  17. package/dist/lib/cli/statelog/redact.d.ts +8 -0
  18. package/dist/lib/cli/statelog/redact.js +21 -0
  19. package/dist/lib/cli/statelog/schedulesClient.d.ts +44 -0
  20. package/dist/lib/cli/statelog/schedulesClient.js +100 -0
  21. package/dist/lib/cli/statelog/secretsClient.d.ts +21 -0
  22. package/dist/lib/cli/statelog/secretsClient.js +94 -0
  23. package/dist/lib/cli/statelog/serveClient.js +45 -44
  24. package/dist/lib/cli/statelog/spendTypes.d.ts +8 -0
  25. package/dist/lib/cli/statelog/spendTypes.js +8 -1
  26. package/dist/lib/cli/statelog/statelogRequest.d.ts +50 -0
  27. package/dist/lib/cli/statelog/statelogRequest.js +87 -0
  28. package/dist/lib/cli/statelog/uploadClient.js +30 -25
  29. package/dist/lib/constants.d.ts +34 -0
  30. package/dist/lib/constants.js +36 -0
  31. package/dist/lib/runtime/audioFormats.d.ts +8 -0
  32. package/dist/lib/runtime/audioFormats.js +16 -0
  33. package/dist/lib/runtime/deterministicClient.d.ts +11 -2
  34. package/dist/lib/runtime/deterministicClient.js +67 -4
  35. package/dist/lib/runtime/invocationUsage.d.ts +15 -1
  36. package/dist/lib/runtime/invocationUsage.js +46 -9
  37. package/dist/lib/runtime/llmClient.d.ts +24 -0
  38. package/dist/lib/runtime/llmClient.js +42 -0
  39. package/dist/lib/runtime/memory/manager.js +4 -2
  40. package/dist/lib/runtime/prompt.js +10 -3
  41. package/dist/lib/runtime/recordPaidUsage.js +5 -2
  42. package/dist/lib/runtime/redactForStatelog.js +10 -2
  43. package/dist/lib/runtime/runBatch.js +8 -0
  44. package/dist/lib/runtime/runner.js +15 -4
  45. package/dist/lib/runtime/state/globalStore.d.ts +35 -6
  46. package/dist/lib/runtime/state/globalStore.js +86 -8
  47. package/dist/lib/statelogClient.d.ts +37 -0
  48. package/dist/lib/statelogClient.js +54 -1
  49. package/dist/lib/stdlib/abortable.d.ts +1 -1
  50. package/dist/lib/stdlib/abortable.js +1 -1
  51. package/dist/lib/stdlib/aws/client.d.ts +49 -0
  52. package/dist/lib/stdlib/aws/client.js +100 -0
  53. package/dist/lib/stdlib/aws/credentials.d.ts +15 -0
  54. package/dist/lib/stdlib/aws/credentials.js +21 -0
  55. package/dist/lib/stdlib/aws/endpoints.d.ts +30 -0
  56. package/dist/lib/stdlib/aws/endpoints.js +73 -0
  57. package/dist/lib/stdlib/aws/errors.d.ts +14 -0
  58. package/dist/lib/stdlib/aws/errors.js +24 -0
  59. package/dist/lib/stdlib/aws/s3.d.ts +61 -0
  60. package/dist/lib/stdlib/aws/s3.js +224 -0
  61. package/dist/lib/stdlib/aws/sigv4.d.ts +51 -0
  62. package/dist/lib/stdlib/aws/sigv4.js +125 -0
  63. package/dist/lib/stdlib/aws/uri.d.ts +8 -0
  64. package/dist/lib/stdlib/aws/uri.js +30 -0
  65. package/dist/lib/stdlib/base64.d.ts +11 -0
  66. package/dist/lib/stdlib/base64.js +20 -0
  67. package/dist/lib/stdlib/builtins.js +9 -7
  68. package/dist/lib/stdlib/http.d.ts +10 -0
  69. package/dist/lib/stdlib/http.js +51 -21
  70. package/dist/lib/stdlib/objectBytes.d.ts +3 -0
  71. package/dist/lib/stdlib/objectBytes.js +12 -0
  72. package/dist/lib/stdlib/speech.d.ts +30 -17
  73. package/dist/lib/stdlib/speech.js +288 -57
  74. package/dist/lib/stdlib/tag.d.ts +1 -1
  75. package/dist/lib/stdlib/tag.js +2 -2
  76. package/dist/lib/stdlib/thread.d.ts +6 -0
  77. package/dist/lib/stdlib/thread.js +12 -2
  78. package/dist/lib/typeChecker/builtins.js +16 -8
  79. package/dist/scripts/agency.js +190 -15
  80. package/package.json +2 -2
  81. package/stdlib/aws/s3.agency +226 -0
  82. package/stdlib/aws/s3.js +1389 -0
  83. package/stdlib/capabilities.agency +4 -1
  84. package/stdlib/capabilities.js +3 -1
  85. package/stdlib/docs/stdlib/aws/s3.md +327 -0
  86. package/stdlib/docs/stdlib/capabilities.md +21 -10
  87. package/stdlib/docs/stdlib/image.md +26 -5
  88. package/stdlib/docs/stdlib/messaging/email.md +4 -4
  89. package/stdlib/docs/stdlib/speech.md +107 -20
  90. package/stdlib/docs/stdlib/tag.md +9 -5
  91. package/stdlib/docs/stdlib/thread.md +68 -22
  92. package/stdlib/image.agency +19 -3
  93. package/stdlib/image.js +7 -3
  94. package/stdlib/messaging/email.agency +104 -49
  95. package/stdlib/messaging/email.js +1 -1
  96. package/stdlib/speech.agency +143 -25
  97. package/stdlib/speech.js +432 -32
  98. package/stdlib/tag.agency +8 -4
  99. package/stdlib/tag.js +51 -8
  100. package/stdlib/thread.agency +35 -1
  101. package/stdlib/thread.js +171 -3
@@ -5,4 +5,8 @@ export type RemoteDeployOptions = {
5
5
  apiKeyEnv?: string;
6
6
  dryRun?: boolean;
7
7
  };
8
- export declare function runDeploy(file: string, options: RemoteDeployOptions, context: RemoteCommandContext): Promise<void>;
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 "deploy an agent with no callable endpoints?" gate. Owns the TTY/readline
2
- // mechanics; `runDeploy` only asks whether it should proceed. Per Revision 2 a
3
- // dry-run and a non-TTY run bypass the prompt and proceed — only an interactive
4
- // TTY asks.
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 ?? defaultPrompt;
19
+ const prompt = options.prompt ?? confirmQuestion;
15
20
  return prompt("Upload anyway?");
16
21
  }
17
- async function defaultPrompt(question) {
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
- return { cron: presetToCron(opts.every), preset: opts.every };
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>;