@alzulejos/laranja 0.2.4

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 (65) hide show
  1. package/README.md +28 -0
  2. package/dist/aws.d.ts +79 -0
  3. package/dist/aws.js +170 -0
  4. package/dist/aws.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +134 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/deploy.d.ts +5 -0
  9. package/dist/commands/deploy.js +146 -0
  10. package/dist/commands/deploy.js.map +1 -0
  11. package/dist/commands/destroy.d.ts +3 -0
  12. package/dist/commands/destroy.js +60 -0
  13. package/dist/commands/destroy.js.map +1 -0
  14. package/dist/commands/eject.d.ts +9 -0
  15. package/dist/commands/eject.js +45 -0
  16. package/dist/commands/eject.js.map +1 -0
  17. package/dist/commands/init.d.ts +1 -0
  18. package/dist/commands/init.js +103 -0
  19. package/dist/commands/init.js.map +1 -0
  20. package/dist/commands/logout.d.ts +5 -0
  21. package/dist/commands/logout.js +15 -0
  22. package/dist/commands/logout.js.map +1 -0
  23. package/dist/commands/logs.d.ts +25 -0
  24. package/dist/commands/logs.js +135 -0
  25. package/dist/commands/logs.js.map +1 -0
  26. package/dist/commands/plan.d.ts +10 -0
  27. package/dist/commands/plan.js +47 -0
  28. package/dist/commands/plan.js.map +1 -0
  29. package/dist/diagnostics.d.ts +31 -0
  30. package/dist/diagnostics.js +85 -0
  31. package/dist/diagnostics.js.map +1 -0
  32. package/dist/index.d.ts +8 -0
  33. package/dist/index.js +8 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/io.d.ts +9 -0
  36. package/dist/io.js +28 -0
  37. package/dist/io.js.map +1 -0
  38. package/dist/iohost.d.ts +36 -0
  39. package/dist/iohost.js +71 -0
  40. package/dist/iohost.js.map +1 -0
  41. package/dist/lifecycle.d.ts +12 -0
  42. package/dist/lifecycle.js +21 -0
  43. package/dist/lifecycle.js.map +1 -0
  44. package/dist/nest-build.d.ts +10 -0
  45. package/dist/nest-build.js +73 -0
  46. package/dist/nest-build.js.map +1 -0
  47. package/dist/pipeline.d.ts +34 -0
  48. package/dist/pipeline.js +115 -0
  49. package/dist/pipeline.js.map +1 -0
  50. package/dist/plan-summary.d.ts +23 -0
  51. package/dist/plan-summary.js +131 -0
  52. package/dist/plan-summary.js.map +1 -0
  53. package/dist/project-link.d.ts +26 -0
  54. package/dist/project-link.js +77 -0
  55. package/dist/project-link.js.map +1 -0
  56. package/dist/report.d.ts +51 -0
  57. package/dist/report.js +183 -0
  58. package/dist/report.js.map +1 -0
  59. package/dist/resource-types.d.ts +9 -0
  60. package/dist/resource-types.js +19 -0
  61. package/dist/resource-types.js.map +1 -0
  62. package/dist/ui.d.ts +50 -0
  63. package/dist/ui.js +216 -0
  64. package/dist/ui.js.map +1 -0
  65. package/package.json +34 -0
@@ -0,0 +1,60 @@
1
+ import { loadConfig, stackName, resolveApiKey, postDestroy, patchDeployment } from "@alzulejos/laranja-core";
2
+ import { getAccountId, deleteStack } from "../aws.js";
3
+ import { reportSafely } from "../lifecycle.js";
4
+ import { step, note } from "../diagnostics.js";
5
+ import { applyAwsEnv, confirm, requireRegion } from "../io.js";
6
+ import * as ui from "../ui.js";
7
+ export async function destroy(projectDir, opts = {}) {
8
+ step("load config");
9
+ const config = await loadConfig(projectDir, { stage: opts.stage });
10
+ const region = requireRegion(config.region);
11
+ applyAwsEnv({ region, profile: config.profile });
12
+ // Destroy talks to the dashboard to open a teardown row, and that call is the
13
+ // permission gate. Require credentials up front (and fail-closed on the gate
14
+ // below) so nothing gets deleted unless the API authenticates and authorizes
15
+ // us first — a missing/revoked key or an unreachable API must stop here.
16
+ const apiKey = resolveApiKey();
17
+ if (!apiKey) {
18
+ throw new Error("Set LARANJA_API_KEY (or run `laranja init`) to destroy.");
19
+ }
20
+ const projectId = config.projectId;
21
+ if (!projectId) {
22
+ throw new Error("This project isn't linked to laranja — run `laranja init` before destroy.");
23
+ }
24
+ step("resolve account");
25
+ const account = await getAccountId(region);
26
+ const name = stackName(config.name, config.stage);
27
+ note({ project: config.name, stage: config.stage, region, account, stackName: name });
28
+ ui.header(`destroy ${config.name} ${ui.dim(config.stage)} ${ui.dim("→")} ${region}`);
29
+ ui.step("🔑", "account", account);
30
+ ui.note(`this will DELETE the "${config.name}" (${config.stage}) stack and its resources.`);
31
+ if (!(await confirm(" are you sure? (y/N)"))) {
32
+ console.log("\n aborted.\n");
33
+ return;
34
+ }
35
+ // Open the teardown row. This is the permission gate: it authenticates the API
36
+ // key and authorizes the destroy against the project. Fail-closed — a bad or
37
+ // revoked key, a forbidden project, or an unreachable API throws here and
38
+ // aborts before we touch CloudFormation. The BE owns the REMOVED resource
39
+ // inventory, so the client only sends the stack identity — no IR, no synth.
40
+ step("open teardown");
41
+ const deploymentId = await postDestroy({ stackName: name, artifact: "cloudformation", provider: "AWS", region }, apiKey, projectId);
42
+ note({ deploymentId });
43
+ await reportSafely("report start", () => patchDeployment(deploymentId, { status: "STARTED", region }, apiKey, projectId));
44
+ // No synth, local or remote — CloudFormation deletes the stack by name.
45
+ step("delete stack");
46
+ const sp = ui.spinner("tearing down stack");
47
+ let existed;
48
+ try {
49
+ existed = await deleteStack(region, name);
50
+ sp.succeed(existed ? "destroyed" : "nothing to destroy (no such stack)");
51
+ }
52
+ catch (err) {
53
+ sp.fail("destroy failed");
54
+ await reportSafely("report failure", () => patchDeployment(deploymentId, { status: "FAILED" }, apiKey, projectId));
55
+ throw err;
56
+ }
57
+ await reportSafely("report success", () => patchDeployment(deploymentId, { status: "SUCCESS" }, apiKey, projectId));
58
+ console.log(`\n ${ui.orange("🧹 gone")}\n`);
59
+ }
60
+ //# sourceMappingURL=destroy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"destroy.js","sourceRoot":"","sources":["../../src/commands/destroy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Generate a standalone, owned CDK project. The project is synthesized on the
3
+ * laranja server (paid; the server gates entitlement and returns 403 if the
4
+ * caller can't eject) — we just write the returned files to `infra/`.
5
+ */
6
+ export declare function eject(projectDir: string, opts: {
7
+ force?: boolean;
8
+ stage?: string;
9
+ }): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { loadConfig, resolveApiKey, postEject, ApiRequestError, apiErrorMessage } from "@alzulejos/laranja-core";
4
+ import { scan } from "@alzulejos/laranja-scanner";
5
+ import { writeResourceTypes } from "../resource-types.js";
6
+ /**
7
+ * Generate a standalone, owned CDK project. The project is synthesized on the
8
+ * laranja server (paid; the server gates entitlement and returns 403 if the
9
+ * caller can't eject) — we just write the returned files to `infra/`.
10
+ */
11
+ export async function eject(projectDir, opts) {
12
+ const config = await loadConfig(projectDir, { stage: opts.stage });
13
+ if (!config.projectId) {
14
+ throw new Error('Set "projectId" in laranja.config.ts (from your dashboard) to eject.');
15
+ }
16
+ const apiKey = resolveApiKey();
17
+ if (!apiKey)
18
+ throw new Error("Set LARANJA_API_KEY (or run `laranja init`) to eject.");
19
+ const ejectDir = path.join(projectDir, "infra");
20
+ if (existsSync(ejectDir) && !opts.force) {
21
+ throw new Error(`${path.relative(projectDir, ejectDir)}/ already exists. Re-run with --force to overwrite.`);
22
+ }
23
+ const ir = scan({ projectDir, config });
24
+ writeResourceTypes(projectDir, ir);
25
+ let res;
26
+ try {
27
+ res = await postEject({ project: ir.app.name, stage: ir.app.stage, artifact: "cdk", ir, assets: {} }, apiKey, config.projectId);
28
+ }
29
+ catch (err) {
30
+ if (err instanceof ApiRequestError)
31
+ throw new Error(apiErrorMessage("Eject failed", err));
32
+ throw err;
33
+ }
34
+ for (const file of res.files) {
35
+ const abs = path.join(ejectDir, file.path);
36
+ mkdirSync(path.dirname(abs), { recursive: true });
37
+ writeFileSync(abs, file.contents);
38
+ }
39
+ console.log(`Ejected ${res.files.length} files to ${path.relative(projectDir, ejectDir)}/`);
40
+ console.log("\nNext:");
41
+ console.log(" cd infra");
42
+ console.log(" npm install");
43
+ console.log(" npm run deploy");
44
+ }
45
+ //# sourceMappingURL=eject.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eject.js","sourceRoot":"","sources":["../../src/commands/eject.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AACjH,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,UAAkB,EAAE,IAAyC;IACvF,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACnE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAC;IAC1F,CAAC;IACD,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAEtF,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAChD,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,qDAAqD,CAAC,CAAC;IAC/G,CAAC;IAED,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;IACxC,kBAAkB,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAEnC,IAAI,GAAG,CAAC;IACR,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,SAAS,CACnB,EAAE,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAC9E,MAAM,EACN,MAAM,CAAC,SAAS,CACjB,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,eAAe;YAAE,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC;QAC1F,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,KAAK,CAAC,MAAM,aAAa,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5F,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC1B,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAC7B,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;AAClC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function init(projectDir: string): Promise<void>;
@@ -0,0 +1,103 @@
1
+ import path from "node:path";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { CONFIG_FILENAME, getMe, resolveApiKey, resolveApiUrl, loadStoredApiKey, storeAuth, ApiRequestError, apiErrorMessage, } from "@alzulejos/laranja-core";
4
+ import { generateResourceTypesStub } from "@alzulejos/laranja-scanner";
5
+ import * as ui from "../ui.js";
6
+ import { RESOURCE_TYPES_FILE } from "../resource-types.js";
7
+ import { resolveProjectId, writeProjectId, writeName } from "../project-link.js";
8
+ const TEMPLATE = `import type { TypedLaranjaConfig } from "./laranja.types.js";
9
+
10
+ const config: TypedLaranjaConfig = {
11
+ // Both filled in from the dashboard project you pick during \`laranja init\`.
12
+ name: "",
13
+ // From your laranja dashboard — identifies this project on the server.
14
+ projectId: "",
15
+ region: "eu-central-1",
16
+ env: {},
17
+ // Emit a CloudWatch dashboard (\`<name>-<stage>\`) with per-function metrics —
18
+ // invocations, errors, throttles, duration. Set false to skip it. Defaults to true.
19
+ monitoring: true,
20
+ // Default compute for every function (the HTTP proxy + each cron/queue).
21
+ compute: { memory: 256, timeout: 30 },
22
+ // Per-resource overrides, keyed by resource id ("http", or a cron/queue id).
23
+ // Filled in once you have resources, e.g.:
24
+ // resources: { cleanup: { memory: 512, timeout: 60 } },
25
+ };
26
+
27
+ export default config;
28
+ `;
29
+ export async function init(projectDir) {
30
+ // Handshake first: validate the API key against the server BEFORE scaffolding
31
+ // any files, so a bad/expired token never leaves a stray laranja.config.ts.
32
+ // Precedence: env var / already-stored key, else prompt for it interactively.
33
+ let apiKey = resolveApiKey();
34
+ if (!apiKey) {
35
+ console.log(`\n ${ui.orange("🍊 Welcome to laranja")} ${ui.dim("·")} ${ui.bold("let's get you set up")}`);
36
+ console.log(` ${ui.dim("Connect this directory to your account and ship in one command.")}\n`);
37
+ console.log(` ${ui.dim("Find your API key in the dashboard under")} ${ui.bold("Account → API keys")}${ui.dim(".")}\n`);
38
+ apiKey = await ui.promptSecret("Paste your laranja API key:");
39
+ if (!apiKey) {
40
+ console.log(`\n ${ui.dim("No API key provided. Set LARANJA_API_KEY (or re-run `laranja init`) to connect your account.")}`);
41
+ return;
42
+ }
43
+ }
44
+ let me;
45
+ try {
46
+ me = await getMe(apiKey);
47
+ }
48
+ catch (err) {
49
+ if (err instanceof ApiRequestError) {
50
+ throw new Error(apiErrorMessage("Handshake failed", err));
51
+ }
52
+ throw err;
53
+ }
54
+ console.log(`\n ${ui.green("✓")} Hi ${ui.bold(me.displayName)}, let's ship something great! 🍊`);
55
+ // Persist the validated key so future commands don't need it re-exported.
56
+ // Skip the write if it's already what's on disk (e.g. supplied via env).
57
+ if (apiKey !== loadStoredApiKey()) {
58
+ const stored = storeAuth({ apiKey, apiUrl: resolveApiUrl() });
59
+ console.log(` ${ui.dim(`Saved your API key to ${stored} — no need to re-export it.`)}`);
60
+ }
61
+ const file = path.join(projectDir, CONFIG_FILENAME);
62
+ const typesFile = path.join(projectDir, RESOURCE_TYPES_FILE);
63
+ const configExists = existsSync(file);
64
+ // Pick the dashboard project BEFORE scaffolding anything, so cancelling the
65
+ // picker never leaves a stray config/types file behind. An existing config is
66
+ // only re-linked when its `projectId` is still empty (never clobber a value).
67
+ const needsLink = !configExists || /projectId:\s*""/.test(readFileSync(file, "utf8"));
68
+ let resolved;
69
+ if (needsLink) {
70
+ resolved = await resolveProjectId(apiKey, me.projects);
71
+ if (!resolved && !configExists) {
72
+ console.log(` ${ui.dim("No project selected — nothing was created. Re-run `laranja init` when you're ready.")}`);
73
+ return;
74
+ }
75
+ }
76
+ // Key is valid and a project is chosen — now it's safe to scaffold the files.
77
+ if (configExists) {
78
+ console.log(`${CONFIG_FILENAME} already exists — nothing to do.`);
79
+ }
80
+ else {
81
+ writeFileSync(file, TEMPLATE);
82
+ console.log(`Created ${CONFIG_FILENAME}.`);
83
+ }
84
+ // The config imports `TypedLaranjaConfig` from here; seed a permissive stub so
85
+ // the import resolves before the first deploy/plan regenerates it with real ids.
86
+ if (!existsSync(typesFile)) {
87
+ writeFileSync(typesFile, generateResourceTypesStub());
88
+ console.log(`Created ${RESOURCE_TYPES_FILE}.`);
89
+ }
90
+ if (resolved) {
91
+ writeProjectId(file, resolved.id);
92
+ writeName(file, resolved.name);
93
+ if (resolved.created) {
94
+ console.log(` ${ui.green("✓")} Created project ${ui.bold(resolved.name)} — it's now on your dashboard.`);
95
+ }
96
+ console.log(` ${ui.dim(`Linked ${CONFIG_FILENAME} to project ${resolved.id}.`)}`);
97
+ }
98
+ else if (needsLink) {
99
+ console.log(` ${ui.dim(`No project selected — set "projectId" in ${CONFIG_FILENAME} before deploying.`)}`);
100
+ }
101
+ console.log(" Next: wrap your app with `export default http(app)`, then run `laranja deploy`.");
102
+ }
103
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Remove the stored API key (~/.laranja/auth.json). Lets you switch accounts —
3
+ * after logging out, the next `laranja init` prompts for a key again.
4
+ */
5
+ export declare function logout(): Promise<void>;
@@ -0,0 +1,15 @@
1
+ import { authFilePath, clearAuth } from "@alzulejos/laranja-core";
2
+ import * as ui from "../ui.js";
3
+ /**
4
+ * Remove the stored API key (~/.laranja/auth.json). Lets you switch accounts —
5
+ * after logging out, the next `laranja init` prompts for a key again.
6
+ */
7
+ export async function logout() {
8
+ if (clearAuth()) {
9
+ console.log(`\n ${ui.green("✓")} Logged out — removed ${ui.dim(authFilePath())}.`);
10
+ }
11
+ else {
12
+ console.log(`\n ${ui.dim("You're not logged in — nothing to remove.")}`);
13
+ }
14
+ }
15
+ //# sourceMappingURL=logout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,UAAU,CAAC;AAE/B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,IAAI,SAAS,EAAE,EAAE,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,yBAAyB,EAAE,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,GAAG,CAAC,CAAC;IACtF,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,2CAA2C,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type DeployedLambda } from "../aws.js";
2
+ export interface LogsOptions {
3
+ /** Function to tail (matched against short label / function name). */
4
+ name?: string;
5
+ /** Tail every function in the stack, multiplexed. */
6
+ all?: boolean;
7
+ /** Live-follow new events. Default true; `--no-follow` dumps history and exits. */
8
+ follow?: boolean;
9
+ /** Look-back window for the historical dump, e.g. "1h", "30m", "10s". */
10
+ since?: string;
11
+ /** Deployment stage — selects which stack's functions to tail. */
12
+ stage?: string;
13
+ }
14
+ export declare function logs(projectDir: string, opts?: LogsOptions): Promise<void>;
15
+ /** Friendly short label for a function: strip the "<app>-" prefix and "-<stage>" suffix. */
16
+ export declare function shortLabel(functionName: string, appName: string, stage: string): string;
17
+ /**
18
+ * Functions matching a user-typed query. Exact matches (full function name or
19
+ * short label) win; only if there are none do we fall back to a substring match
20
+ * — otherwise typing "app" would match every function whose app name contains
21
+ * it. Pure — the impure picker lives separately.
22
+ */
23
+ export declare function matchByName(fns: DeployedLambda[], query: string, label: (f: DeployedLambda) => string): DeployedLambda[];
24
+ /** Parse a duration like "1h" / "30m" / "10s" / "2d" into milliseconds. */
25
+ export declare function parseSince(s: string): number;
@@ -0,0 +1,135 @@
1
+ import { CloudWatchLogsClient, StartLiveTailCommand, FilterLogEventsCommand, } from "@aws-sdk/client-cloudwatch-logs";
2
+ import { loadConfig, stackName } from "@alzulejos/laranja-core";
3
+ import { getAccountId, listStackLambdas } from "../aws.js";
4
+ import { applyAwsEnv, requireRegion } from "../io.js";
5
+ import * as ui from "../ui.js";
6
+ const EMOJI = { http: "🌐", cron: "⏰", queue: "📨", lambda: "λ" };
7
+ export async function logs(projectDir, opts = {}) {
8
+ const config = await loadConfig(projectDir, { stage: opts.stage });
9
+ const region = requireRegion(config.region);
10
+ applyAwsEnv({ region, profile: config.profile });
11
+ const stack = stackName(config.name, config.stage);
12
+ ui.header(`logs ${config.name} ${ui.dim(config.stage)} ${ui.dim("→")} ${region}`);
13
+ // The live CloudFormation stack is the source of truth — no local state needed.
14
+ const sp = ui.spinner("finding functions");
15
+ let fns;
16
+ try {
17
+ fns = await listStackLambdas(region, stack);
18
+ }
19
+ finally {
20
+ sp.stop(); // always clear the spinner, even when discovery throws
21
+ }
22
+ if (fns.length === 0)
23
+ throw new Error(`Stack "${stack}" has no Lambda functions.`);
24
+ const label = (f) => shortLabel(f.functionName, config.name, config.stage);
25
+ const targets = await chooseTargets(fns, label, opts);
26
+ if (!targets) {
27
+ console.log(` ${ui.dim("cancelled.")}\n`);
28
+ return;
29
+ }
30
+ const labelOf = new Map(targets.map((t) => [t.logGroupName, label(t)]));
31
+ const client = new CloudWatchLogsClient({ region });
32
+ // `--since` (or `--no-follow`) prints a historical window first.
33
+ if (opts.since || opts.follow === false) {
34
+ await dumpHistory(client, targets, labelOf, parseSince(opts.since ?? "1h"));
35
+ if (opts.follow === false)
36
+ return;
37
+ }
38
+ const account = await getAccountId(region);
39
+ await liveTail(client, region, account, targets, labelOf);
40
+ }
41
+ /** Friendly short label for a function: strip the "<app>-" prefix and "-<stage>" suffix. */
42
+ export function shortLabel(functionName, appName, stage) {
43
+ return functionName
44
+ .replace(new RegExp(`^${appName}-`), "")
45
+ .replace(new RegExp(`-${stage}$`), "");
46
+ }
47
+ /**
48
+ * Functions matching a user-typed query. Exact matches (full function name or
49
+ * short label) win; only if there are none do we fall back to a substring match
50
+ * — otherwise typing "app" would match every function whose app name contains
51
+ * it. Pure — the impure picker lives separately.
52
+ */
53
+ export function matchByName(fns, query, label) {
54
+ const exact = fns.filter((f) => f.functionName === query || label(f) === query);
55
+ return exact.length > 0 ? exact : fns.filter((f) => f.functionName.includes(query));
56
+ }
57
+ /** Resolve which functions to tail: --all, a name match, or an interactive pick. */
58
+ async function chooseTargets(fns, label, opts) {
59
+ if (opts.all)
60
+ return fns;
61
+ if (opts.name) {
62
+ const match = matchByName(fns, opts.name, label);
63
+ if (match.length === 0) {
64
+ throw new Error(`No function matching "${opts.name}". Available: ${fns.map((f) => label(f)).join(", ")}`);
65
+ }
66
+ return match;
67
+ }
68
+ // No name given: interactive picker (requires a TTY).
69
+ if (!process.stdin.isTTY) {
70
+ throw new Error(`Specify a function (e.g. \`laranja logs ${label(fns[0])}\`) or --all. ` +
71
+ `Available: ${fns.map((f) => label(f)).join(", ")}`);
72
+ }
73
+ const choices = [
74
+ ...fns.map((f) => ({ label: `${EMOJI[f.kind]} ${label(f).padEnd(20)} ${ui.dim(f.functionName)}`, value: [f] })),
75
+ { label: `📚 ${"all functions".padEnd(20)} ${ui.dim(`${fns.length} log groups`)}`, value: fns },
76
+ ];
77
+ return ui.select("select a function to tail", choices);
78
+ }
79
+ /** Print recent events from the given window, oldest-first, then return. */
80
+ async function dumpHistory(client, targets, labelOf, sinceMs) {
81
+ const startTime = Date.now() - sinceMs;
82
+ const multi = targets.length > 1;
83
+ const rows = [];
84
+ for (const t of targets) {
85
+ let nextToken;
86
+ do {
87
+ const res = await client.send(new FilterLogEventsCommand({ logGroupName: t.logGroupName, startTime, nextToken }));
88
+ for (const e of res.events ?? []) {
89
+ rows.push({ timestamp: e.timestamp ?? 0, message: e.message, group: t.logGroupName });
90
+ }
91
+ nextToken = res.nextToken;
92
+ } while (nextToken);
93
+ }
94
+ rows.sort((a, b) => a.timestamp - b.timestamp);
95
+ for (const r of rows)
96
+ printEvent(r.timestamp, r.message, multi ? labelOf.get(r.group) : undefined);
97
+ if (rows.length === 0)
98
+ console.log(` ${ui.dim("(no events in window)")}`);
99
+ }
100
+ /** Stream new events live until the process is interrupted (Ctrl-C). */
101
+ async function liveTail(client, region, account, targets, labelOf) {
102
+ const multi = targets.length > 1;
103
+ const identifiers = targets.map((t) => `arn:aws:logs:${region}:${account}:log-group:${t.logGroupName}`);
104
+ // Map back from the ARN identifier the API echoes to our short label.
105
+ const labelByArn = new Map(targets.map((t) => [`arn:aws:logs:${region}:${account}:log-group:${t.logGroupName}`, labelOf.get(t.logGroupName)]));
106
+ const res = await client.send(new StartLiveTailCommand({ logGroupIdentifiers: identifiers }));
107
+ console.log(` ${ui.green("●")} ${ui.dim(`tailing ${targets.length === 1 ? labelOf.get(targets[0].logGroupName) : `${targets.length} functions`} — Ctrl-C to stop`)}\n`);
108
+ const stream = res.responseStream;
109
+ if (!stream)
110
+ return;
111
+ for await (const event of stream) {
112
+ for (const e of event.sessionUpdate?.sessionResults ?? []) {
113
+ printEvent(e.timestamp ?? 0, e.message, multi ? labelByArn.get(e.logGroupIdentifier ?? "") : undefined);
114
+ }
115
+ }
116
+ }
117
+ /** Print one log line: dim timestamp, optional function tag, message. */
118
+ function printEvent(timestamp, message, tag) {
119
+ const msg = (message ?? "").replace(/\n$/, "");
120
+ if (!msg)
121
+ return;
122
+ const ts = ui.dim(new Date(timestamp).toISOString().slice(11, 23));
123
+ const prefix = tag ? `${ui.cyan(tag)} ` : "";
124
+ console.log(` ${ts} ${prefix}${msg}`);
125
+ }
126
+ /** Parse a duration like "1h" / "30m" / "10s" / "2d" into milliseconds. */
127
+ export function parseSince(s) {
128
+ const m = /^(\d+)(s|m|h|d)$/.exec(s.trim());
129
+ if (!m)
130
+ throw new Error(`Invalid --since "${s}". Use e.g. 30s, 15m, 1h, 2d.`);
131
+ const n = Number(m[1]);
132
+ const unit = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[m[2]];
133
+ return n * unit;
134
+ }
135
+ //# sourceMappingURL=logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.js","sourceRoot":"","sources":["../../src/commands/logs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Show what a deploy would do: synthesize the template on the laranja server
3
+ * (read-only — no deployment row, no quota), diff it against the stack currently
4
+ * deployed in your AWS account, and print the laranja table with each resource
5
+ * tagged created / changed / unchanged. Needs `LARANJA_API_KEY` (for the synth)
6
+ * and AWS credentials (to read the live stack). Nothing is applied.
7
+ */
8
+ export declare function plan(projectDir: string, opts?: {
9
+ stage?: string;
10
+ }): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import { Toolkit, StackSelectionStrategy } from "@aws-cdk/toolkit-lib";
2
+ import { loadConfig, resolveApiKey } from "@alzulejos/laranja-core";
3
+ import { buildPlanAssembly } from "../pipeline.js";
4
+ import { getAccountId } from "../aws.js";
5
+ import { applyAwsEnv, requireRegion } from "../io.js";
6
+ import { summarizePlan } from "../plan-summary.js";
7
+ import * as ui from "../ui.js";
8
+ /** Swallows the toolkit's own diff output — we render our own summary instead. */
9
+ const silentIoHost = {
10
+ async notify() { },
11
+ async requestResponse(msg) {
12
+ return msg.defaultResponse;
13
+ },
14
+ };
15
+ /**
16
+ * Show what a deploy would do: synthesize the template on the laranja server
17
+ * (read-only — no deployment row, no quota), diff it against the stack currently
18
+ * deployed in your AWS account, and print the laranja table with each resource
19
+ * tagged created / changed / unchanged. Needs `LARANJA_API_KEY` (for the synth)
20
+ * and AWS credentials (to read the live stack). Nothing is applied.
21
+ */
22
+ export async function plan(projectDir, opts = {}) {
23
+ const apiKey = resolveApiKey();
24
+ if (!apiKey)
25
+ throw new Error("Set LARANJA_API_KEY (or run `laranja init`) to plan.");
26
+ // loadConfig raises a clear "run `laranja init`" error if this directory isn't
27
+ // linked yet (empty name/projectId); pipeline enforces projectId before synth.
28
+ const config = await loadConfig(projectDir, { stage: opts.stage });
29
+ const region = requireRegion(config.region);
30
+ applyAwsEnv({ region, profile: config.profile });
31
+ const sp = ui.spinner("diffing against your deployed stack");
32
+ try {
33
+ const account = await getAccountId(region);
34
+ const { ir, stackName, cdkOutDir, template } = await buildPlanAssembly(projectDir, { region, account, stage: opts.stage }, apiKey);
35
+ const toolkit = new Toolkit({ ioHost: silentIoHost });
36
+ const cx = await toolkit.fromAssemblyDirectory(cdkOutDir);
37
+ const diffs = await toolkit.diff(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } });
38
+ const diff = (diffs[stackName] ?? Object.values(diffs)[0]);
39
+ sp.stop();
40
+ summarizePlan(ir, template ?? {}, diff ?? { resources: { changes: {} } });
41
+ }
42
+ catch (err) {
43
+ sp.fail("plan failed");
44
+ throw err;
45
+ }
46
+ }
47
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/commands/plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,aAAa,EAAsB,MAAM,oBAAoB,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,UAAU,CAAC;AAE/B,kFAAkF;AAClF,MAAM,YAAY,GAAG;IACnB,KAAK,CAAC,MAAM,KAAmB,CAAC;IAChC,KAAK,CAAC,eAAe,CAAI,GAA2B;QAClD,OAAO,GAAG,CAAC,eAAe,CAAC;IAC7B,CAAC;CACF,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,UAAkB,EAAE,OAA2B,EAAE;IAC1E,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAErF,+EAA+E;IAC/E,+EAA+E;IAC/E,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAEjD,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,qCAAqC,CAAC,CAAC;IAC7D,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,iBAAiB,CACpE,UAAU,EACV,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EACtC,MAAM,CACP,CAAC;QAEF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QACtD,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAC;QAC1D,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,sBAAsB,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAClG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAA8B,CAAC;QAExF,EAAE,CAAC,IAAI,EAAE,CAAC;QACV,aAAa,CAAC,EAAE,EAAE,QAAQ,IAAI,EAAE,EAAE,IAAI,IAAI,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACvB,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ /** Begin tracking a command run (called by the dispatcher before the command). */
2
+ export declare function beginRun(command: string, projectDir?: string): void;
3
+ /** Mark the current step; it's surfaced in the failure report if something throws. */
4
+ export declare function step(name: string): void;
5
+ /** Attach context to the run (project, stage, region, deploymentId, …). */
6
+ export declare function note(fields: Record<string, unknown>): void;
7
+ export interface FailureReport {
8
+ command: string;
9
+ step: string;
10
+ reason: string;
11
+ errorName?: string;
12
+ stack?: string;
13
+ durationMs: number;
14
+ at: string;
15
+ fields: Record<string, unknown>;
16
+ }
17
+ /** Build the structured report for a thrown error against the current run. */
18
+ export declare function buildFailureReport(err: unknown): FailureReport;
19
+ /**
20
+ * Append the report as one JSON line to `~/.laranja/errors.jsonl`. Best-effort:
21
+ * returns the path written, or undefined if the log couldn't be written (logging
22
+ * a failure must never itself crash the CLI).
23
+ */
24
+ export declare function writeFailureReport(report: FailureReport): string | undefined;
25
+ /**
26
+ * POST the report to the dashboard (`/report`), scoped to the user (api key) +
27
+ * project (project id from the run's config). Best-effort: returns whether it was
28
+ * sent. Skipped silently when not logged in or the project has no id — and never
29
+ * throws, so a failed send can't compound the original failure.
30
+ */
31
+ export declare function sendFailureReport(report: FailureReport): Promise<boolean>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Lightweight failure diagnostics. The CLI runs one command per process, so a
3
+ * single module-level "current run" tracks which command is executing, which
4
+ * step it's on, and any context worth attaching (project, region, deployment id).
5
+ *
6
+ * When a command throws, the top-level handler builds a structured report and
7
+ * appends it to `~/.laranja/errors.jsonl` — so a half-way failure leaves a record
8
+ * of WHAT failed, in WHICH step, and WHY. Shaped so we can also POST it to the
9
+ * dashboard later; for now it's logged locally.
10
+ */
11
+ import path from "node:path";
12
+ import { appendFileSync, mkdirSync } from "node:fs";
13
+ import { authDir, loadConfig, resolveApiKey, postReport } from "@alzulejos/laranja-core";
14
+ let run = { command: "", step: "starting", startedAt: Date.now(), fields: {} };
15
+ /** Begin tracking a command run (called by the dispatcher before the command). */
16
+ export function beginRun(command, projectDir) {
17
+ run = { command, projectDir, step: "starting", startedAt: Date.now(), fields: {} };
18
+ }
19
+ /** Mark the current step; it's surfaced in the failure report if something throws. */
20
+ export function step(name) {
21
+ run.step = name;
22
+ }
23
+ /** Attach context to the run (project, stage, region, deploymentId, …). */
24
+ export function note(fields) {
25
+ Object.assign(run.fields, fields);
26
+ }
27
+ /** Build the structured report for a thrown error against the current run. */
28
+ export function buildFailureReport(err) {
29
+ const e = err instanceof Error ? err : undefined;
30
+ return {
31
+ command: run.command,
32
+ step: run.step,
33
+ reason: e?.message ?? String(err),
34
+ errorName: e?.name,
35
+ stack: e?.stack,
36
+ durationMs: Date.now() - run.startedAt,
37
+ at: new Date().toISOString(),
38
+ fields: run.fields,
39
+ };
40
+ }
41
+ /**
42
+ * Append the report as one JSON line to `~/.laranja/errors.jsonl`. Best-effort:
43
+ * returns the path written, or undefined if the log couldn't be written (logging
44
+ * a failure must never itself crash the CLI).
45
+ */
46
+ export function writeFailureReport(report) {
47
+ try {
48
+ const dir = authDir();
49
+ mkdirSync(dir, { recursive: true });
50
+ const file = path.join(dir, "errors.jsonl");
51
+ appendFileSync(file, JSON.stringify(report) + "\n");
52
+ return file;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ /**
59
+ * POST the report to the dashboard (`/report`), scoped to the user (api key) +
60
+ * project (project id from the run's config). Best-effort: returns whether it was
61
+ * sent. Skipped silently when not logged in or the project has no id — and never
62
+ * throws, so a failed send can't compound the original failure.
63
+ */
64
+ export async function sendFailureReport(report) {
65
+ const apiKey = resolveApiKey();
66
+ if (!apiKey || !run.projectDir)
67
+ return false;
68
+ let projectId;
69
+ try {
70
+ projectId = (await loadConfig(run.projectDir)).projectId;
71
+ }
72
+ catch {
73
+ return false; // no/unreadable config — nothing to scope the report to
74
+ }
75
+ if (!projectId)
76
+ return false;
77
+ try {
78
+ await postReport(report, apiKey, projectId);
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ //# sourceMappingURL=diagnostics.js.map