@faable/faable 1.37.0 → 1.38.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.
@@ -151,6 +151,12 @@ class FaableApi {
151
151
  async getDeployment(deployment_id) {
152
152
  return data(this.client.get(`/deployment/${deployment_id}`));
153
153
  }
154
+ // Build payload of a deployment (runnable descriptor: runtime, profile,
155
+ // size, checksum). Authorized by the caller's access to the parent
156
+ // deployment, so no team header is needed — same posture as getDeployment.
157
+ async getArtifact(artifact_id) {
158
+ return data(this.client.get(`/artifact/${artifact_id}`));
159
+ }
154
160
  // Attach the captured build/deploy output to a deployment. The base client
155
161
  // timeout (10s) is too short for a multi-MB body on a slow uplink.
156
162
  async uploadDeploymentLogs(deployment_id, body) {
@@ -5,6 +5,7 @@ import { link } from '../link/index.js';
5
5
  import { domains } from './domains/index.js';
6
6
  import { git_context } from './git_context.js';
7
7
  import { deployments } from './inspect/deployments.js';
8
+ import { inspect } from './inspect/inspect.js';
8
9
  import { apps_list } from './inspect/list.js';
9
10
  import { logs } from './inspect/logs.js';
10
11
  import { open_app } from './inspect/open.js';
@@ -32,6 +33,7 @@ const deploy = {
32
33
  .command(status)
33
34
  .command(apps_list)
34
35
  .command(deployments)
36
+ .command(inspect)
35
37
  .command(open_app)
36
38
  .command(trigger)
37
39
  .command(redeploy)
@@ -34,6 +34,7 @@ const deployments = {
34
34
  const live = d.id === app.status?.deployment ? ' ← live' : '';
35
35
  log.info(` ${deployment_row(d)}${live}`);
36
36
  }
37
+ log.info(`Full record of one: faable deploy inspect <deployment_id>`);
37
38
  }
38
39
  };
39
40
 
@@ -0,0 +1,122 @@
1
+ import { phase_badge, when, short_commit, detected_summary } from './format.js';
2
+
3
+ // Pure rendering of `faable deploy inspect` — everything the platform
4
+ // recorded about ONE deployment, in reading order. Kept out of the handler
5
+ // so the layout is testable without an API.
6
+ const format_bytes = (bytes) => {
7
+ if (!bytes || bytes < 0)
8
+ return '-';
9
+ const units = ['B', 'KB', 'MB', 'GB'];
10
+ let value = bytes;
11
+ let unit = 0;
12
+ while (value >= 1024 && unit < units.length - 1) {
13
+ value /= 1024;
14
+ unit++;
15
+ }
16
+ return `${unit === 0 ? value : value.toFixed(1)} ${units[unit]}`;
17
+ };
18
+ // "refs/heads/main" → "main"; a bare branch name passes through.
19
+ const short_ref = (ref) => ref ? ref.replace(/^refs\/(heads|tags)\//, '') : null;
20
+ // "next-standalone · node 22 · 41.2 MB" from the deploy-v3 runnable
21
+ // descriptor; null while the build has not sealed one yet.
22
+ const artifact_summary = (artifact) => {
23
+ const a = artifact?.artifact;
24
+ if (!a)
25
+ return null;
26
+ const runtime = [a.runtime.name, a.runtime.version].filter(Boolean).join(' ');
27
+ return [a.profile, runtime, format_bytes(a.size)].filter(Boolean).join(' · ');
28
+ };
29
+ // First line of a commit message (the subject), quoted.
30
+ const commit_subject = (message) => {
31
+ const subject = message?.split('\n')[0].trim();
32
+ return subject ? `"${subject}"` : null;
33
+ };
34
+ const label = (name, value) => ` ${`${name}:`.padEnd(12)}${value}`;
35
+ // Command suggestions that match the phase: what you would actually run next,
36
+ // rendered as labeled lines so they stay in the same column as the record.
37
+ // A BUILD_ERROR never ran, so it has no runtime logs to offer; a retired
38
+ // deployment does (for 24h — the runtime window), plus its frozen build output.
39
+ const next_steps = (d, is_live) => {
40
+ const phase = d.status?.phase ?? '';
41
+ const runtime = label('Logs', `faable deploy logs -d ${d.id}`);
42
+ const build = label('Build', `faable deploy logs --build -d ${d.id}`);
43
+ const retry = label('Retry', `faable deploy redeploy ${d.id}`);
44
+ if (phase === 'BUILD_ERROR')
45
+ return [build, retry];
46
+ if (phase === 'ERROR')
47
+ return [runtime, build, retry];
48
+ if (phase === 'BUILDING' || phase === 'QUEUED' || phase === 'UNKNOWN') {
49
+ return [label('Follow', `faable deploy logs --build -d ${d.id} --follow`)];
50
+ }
51
+ if (is_live)
52
+ return [label('Logs', 'faable deploy logs')];
53
+ return [runtime, build];
54
+ };
55
+ const deployment_detail = (args) => {
56
+ const { app, deployment: d, artifact } = args;
57
+ const is_live = d.id === app.status?.deployment;
58
+ const lines = [];
59
+ lines.push(`${phase_badge(d.status?.phase)} ${d.id}`);
60
+ lines.push(label('App', `${app.name} (${app.id})`));
61
+ if (d.createdAt) {
62
+ lines.push(label('Created', `${when(d.createdAt)} (${d.createdAt})`));
63
+ }
64
+ lines.push(label('Trigger', d.trigger === 'webhook' ? 'push (webhook)' : 'cli'));
65
+ lines.push(label('Serving', is_live ? `yes — https://${app.url}` : 'no (not live)'));
66
+ if (d.github_commit) {
67
+ const attribution = [
68
+ short_ref(d.github_ref),
69
+ d.github_actor && `by ${d.github_actor}`
70
+ ]
71
+ .filter(Boolean)
72
+ .join(' ');
73
+ const commit = [
74
+ short_commit(d.github_commit),
75
+ commit_subject(d.github_commit_message),
76
+ attribution && `(${attribution})`
77
+ ]
78
+ .filter(Boolean)
79
+ .join(' ');
80
+ lines.push(label('Commit', commit));
81
+ }
82
+ if (d.release)
83
+ lines.push(label('Release', d.release));
84
+ // Stack the builder detected for THIS build (falls back to the app's, which
85
+ // is the last successful detection).
86
+ const stack = detected_summary(d.detected ?? app.detected);
87
+ if (stack)
88
+ lines.push(label('Stack', stack));
89
+ const runnable = artifact_summary(artifact);
90
+ if (runnable) {
91
+ const ready = d.artifact_ready_at
92
+ ? ` (sealed ${when(d.artifact_ready_at)})`
93
+ : '';
94
+ lines.push(label('Artifact', `${runnable}${ready}`));
95
+ }
96
+ if (artifact?.artifact?.start_command) {
97
+ lines.push(label('Start', artifact.artifact.start_command));
98
+ }
99
+ if (artifact?.purged_at) {
100
+ lines.push(label('Purged', `${when(artifact.purged_at)} — source no longer stored`));
101
+ }
102
+ if (d.image)
103
+ lines.push(label('Image', d.image));
104
+ if (d.status?.runtime_image) {
105
+ lines.push(label('Runtime', d.status.runtime_image));
106
+ }
107
+ if (d.redeploy_of)
108
+ lines.push(label('Rebuild of', d.redeploy_of));
109
+ if (d.quota_released_at) {
110
+ lines.push(label('Quota', `hold released ${when(d.quota_released_at)}`));
111
+ }
112
+ if (d.status?.reason) {
113
+ lines.push(' Reason:');
114
+ for (const line of d.status.reason.split('\n')) {
115
+ lines.push(` ${line}`);
116
+ }
117
+ }
118
+ lines.push(...next_steps(d, is_live));
119
+ return lines;
120
+ };
121
+
122
+ export { artifact_summary, deployment_detail, format_bytes, short_ref };
@@ -0,0 +1,67 @@
1
+ import { requireApi } from '../../../api/context.js';
2
+ import { log } from '../../../log.js';
3
+ import { resolve_app_id } from '../resolve_app_id.js';
4
+ import { deployment_detail } from './detail.js';
5
+
6
+ const inspect = {
7
+ command: 'inspect [deployment]',
8
+ describe: 'Show everything recorded about one deployment',
9
+ builder: yargs => yargs
10
+ .positional('deployment', {
11
+ type: 'string',
12
+ description: 'Deployment id (defaults to the latest one of the app)'
13
+ })
14
+ .option('app', {
15
+ alias: 'a',
16
+ type: 'string',
17
+ description: 'App Identifier (defaults to the linked app)'
18
+ })
19
+ .option('json', {
20
+ type: 'boolean',
21
+ default: false,
22
+ description: 'Output raw JSON (for scripting)'
23
+ })
24
+ .example('$0 deploy inspect deployment_a1b2c3', 'Everything about that deployment: phase, commit, artifact, failure reason')
25
+ .example('$0 deploy inspect', 'Same, for the latest deployment')
26
+ .showHelpOnFail(false),
27
+ handler: async (args) => {
28
+ const ctx = await requireApi();
29
+ const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
30
+ const app = await ctx.api.getApp(app_id);
31
+ let deployment_id = args.deployment;
32
+ if (!deployment_id) {
33
+ const rows = await ctx.api.listDeployments(app_id, app.team);
34
+ deployment_id = rows[0]?.id;
35
+ if (!deployment_id) {
36
+ log.info(`📭 ${app.name} has no deployments yet.`);
37
+ return;
38
+ }
39
+ }
40
+ const deployment = await ctx.api.getDeployment(deployment_id);
41
+ // A deployment id is globally unique but the app comes from the link/--app
42
+ // flag: refuse to render one against the wrong app instead of printing a
43
+ // mismatched "Serving"/"App" header.
44
+ if (deployment.app_id && deployment.app_id !== app_id) {
45
+ throw new Error(`${deployment_id} belongs to another app (${deployment.app_id}), not ${app.name} (${app_id}). Pass --app ${deployment.app_id}.`);
46
+ }
47
+ // The runnable descriptor (runtime, profile, size) lives on the artifact
48
+ // row. Absent on image deploys, and a purged/expired row must not break
49
+ // the read — the rest of the record is still worth showing.
50
+ let artifact = null;
51
+ if (deployment.artifact_id) {
52
+ artifact = await ctx.api.getArtifact(deployment.artifact_id).catch(() => {
53
+ log.debug(`Could not read artifact ${deployment.artifact_id}`);
54
+ return null;
55
+ });
56
+ }
57
+ if (args.json) {
58
+ process.stdout.write(JSON.stringify({ ...deployment, artifact }, null, 2) + '\n');
59
+ return;
60
+ }
61
+ for (const line of deployment_detail({ app, deployment, artifact })) {
62
+ log.info(line);
63
+ }
64
+ }
65
+ };
66
+
67
+ export { inspect };
@@ -33,6 +33,7 @@ const logs = {
33
33
  description: 'With --build: keep tailing the output while the build runs'
34
34
  })
35
35
  .example('$0 deploy logs', 'Runtime logs of the linked app (last 24h)')
36
+ .example('$0 deploy logs -d deployment_a1b2c3', 'Runtime logs of ONE deployment (last 24h)')
36
37
  .example('$0 deploy logs --build', 'Build output of the latest deployment')
37
38
  .example('$0 deploy logs --build --follow', 'Tail the build that is running right now')
38
39
  .showHelpOnFail(false),
@@ -87,12 +88,17 @@ const logs = {
87
88
  const lines = await ctx.api.getAppLogs(app_id, {
88
89
  deployment_id: args.deployment
89
90
  });
91
+ // Say WHICH scope came back empty: with -d the 24h window is usually the
92
+ // reason (a retired deployment stopped writing when it stopped serving).
93
+ const scope = args.deployment
94
+ ? `${args.deployment} (${app.name})`
95
+ : `${app.name} (${app_id})`;
90
96
  if (lines.length === 0) {
91
- log.info(`📭 No runtime logs in the last 24h for ${app.name} (${app_id}).`);
92
- log.info(`For build output, use: faable deploy logs --build`);
97
+ log.info(`📭 No runtime logs in the last 24h for ${scope}.`);
98
+ log.info(`For build output, use: faable deploy logs --build${args.deployment ? ` -d ${args.deployment}` : ''}`);
93
99
  return;
94
100
  }
95
- log.info(`📜 Runtime logs of ${app.name} (last 24h, newest last):`);
101
+ log.info(`📜 Runtime logs of ${app.name}${args.deployment ? ` · ${args.deployment}` : ''} (last 24h, newest last):`);
96
102
  for (const line of format_log_lines(lines)) {
97
103
  process.stdout.write(line + '\n');
98
104
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",