void 0.10.3 → 0.10.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.
package/dist/cli/cli.mjs CHANGED
@@ -635,6 +635,11 @@ const githubSubcommands = [
635
635
  arg: "[project]",
636
636
  description: "Update an existing connection's branch, build executor, or deploy workflow"
637
637
  },
638
+ {
639
+ name: "status",
640
+ arg: "[project]",
641
+ description: "Show a project's GitHub connection (repo, branch, executor, workflow)"
642
+ },
638
643
  {
639
644
  name: "link",
640
645
  description: "Link your GitHub account to Void"
@@ -750,7 +755,26 @@ function parseGithubArgs(args) {
750
755
  workflowPath
751
756
  };
752
757
  }
753
- throw new Error("github: Usage: void github <install|installations|connect|update|link>.");
758
+ if (subcommand === "status") {
759
+ let projectName = "";
760
+ for (let i = 0; i < rest.length; i++) {
761
+ const arg = rest[i];
762
+ if (arg === "--project") {
763
+ if (i + 1 >= rest.length) throw new Error("github: --project requires a value.");
764
+ const value = rest[++i];
765
+ if (value.trim() === "") throw new Error("github: --project requires a value.");
766
+ if (projectName) throw new Error(`github: Unexpected argument '${value}'.`);
767
+ projectName = value;
768
+ } else if (arg.startsWith("-")) throw new Error(`github: Unexpected argument '${arg}'.`);
769
+ else if (projectName) throw new Error(`github: Unexpected argument '${arg}'.`);
770
+ else projectName = arg;
771
+ }
772
+ return {
773
+ subcommand,
774
+ projectName
775
+ };
776
+ }
777
+ throw new Error("github: Usage: void github <install|installations|connect|update|status|link>.");
754
778
  }
755
779
  //#endregion
756
780
  //#region src/cli/ascii.ts
@@ -1064,6 +1088,10 @@ const HELP_TREE = {
1064
1088
  command: "update [project]",
1065
1089
  description: "Update an existing connection's branch, build executor, or deploy workflow"
1066
1090
  },
1091
+ {
1092
+ command: "status [project]",
1093
+ description: "Show a project's GitHub connection (repo, branch, executor, workflow)"
1094
+ },
1067
1095
  {
1068
1096
  command: "link",
1069
1097
  description: "Link your GitHub account to Void"
@@ -1095,20 +1123,27 @@ const HELP_TREE = {
1095
1123
  },
1096
1124
  {
1097
1125
  command: "--executor <type>",
1098
- description: "Build executor: container (default) | github_actions"
1126
+ description: "Build executor: container (default) | github_actions; prompted interactively when omitted"
1099
1127
  },
1100
1128
  {
1101
1129
  command: "--workflow <path>",
1102
- description: "Authorized deploy workflow file (default .github/workflows/void-deploy.yml)"
1130
+ description: "Authorized deploy workflow file (default .github/workflows/void-deploy.yml); prompted interactively when using github_actions"
1103
1131
  }
1104
1132
  ],
1105
- examples: [{
1106
- command: "void github connect [project]",
1107
- description: "Connect a repository to a project"
1108
- }, {
1109
- command: "void github connect my-app --installation 42 --repo owner/repo --branch main",
1110
- description: "Non-interactive connect (all flags explicit)"
1111
- }]
1133
+ examples: [
1134
+ {
1135
+ command: "void github connect [project]",
1136
+ description: "Connect a repository to a project"
1137
+ },
1138
+ {
1139
+ command: "void github connect my-app --installation 42 --repo owner/repo --branch main",
1140
+ description: "Non-interactive connect (all flags explicit)"
1141
+ },
1142
+ {
1143
+ command: "void github connect my-app --repo owner/repo --branch main --executor github_actions",
1144
+ description: "Non-interactive connect using the GitHub Actions executor"
1145
+ }
1146
+ ]
1112
1147
  },
1113
1148
  update: {
1114
1149
  summary: "Update an existing connection's branch, build executor, or deploy workflow.",
@@ -1124,11 +1159,11 @@ const HELP_TREE = {
1124
1159
  },
1125
1160
  {
1126
1161
  command: "--executor <type>",
1127
- description: "New build executor: container | github_actions"
1162
+ description: "New build executor: container | github_actions; prompted interactively when omitted"
1128
1163
  },
1129
1164
  {
1130
1165
  command: "--workflow <path>",
1131
- description: "New authorized deploy workflow file (under .github/workflows/)"
1166
+ description: "New authorized deploy workflow file under .github/workflows/; prompted interactively when using github_actions"
1132
1167
  }
1133
1168
  ],
1134
1169
  examples: [{
@@ -1138,6 +1173,21 @@ const HELP_TREE = {
1138
1173
  command: "void github update my-app --branch release",
1139
1174
  description: "Change the deploy branch"
1140
1175
  }]
1176
+ },
1177
+ status: {
1178
+ summary: "Show a project's GitHub connection settings.",
1179
+ usage: ["void github status [project] [options]"],
1180
+ options: [{
1181
+ command: "--project <name>",
1182
+ description: "Project name (alias for positional)"
1183
+ }],
1184
+ examples: [{
1185
+ command: "void github status",
1186
+ description: "Show the linked project's connection"
1187
+ }, {
1188
+ command: "void github status my-app",
1189
+ description: "Show a specific project connection"
1190
+ }]
1141
1191
  }
1142
1192
  }
1143
1193
  },
@@ -1906,7 +1956,7 @@ else if (command === "domain") runSubcommandGroup("domain", domainSubcommands, p
1906
1956
  else if (command === "db") runSubcommandGroup("db", dbSubcommands, parseDbArgs, (root, parsed) => import("../db-hAmBumot.mjs").then(({ runDbCommand }) => runDbCommand(root, parsed)));
1907
1957
  else if (command === "env") runSubcommandGroup("env", envSubcommands, parseEnvArgs, (root, parsed) => import("../env-CIwG2y7G.mjs").then(({ runEnvCommand }) => runEnvCommand(root, parsed)));
1908
1958
  else if (command === "gen") runSubcommandGroup("gen", genSubcommands, parseGenArgs, (root, parsed) => import("../gen-q6McxXgE.mjs").then(({ runGenCommand }) => runGenCommand(root, parsed)));
1909
- else if (command === "github") runSubcommandGroup("github", githubSubcommands, parseGithubArgs, (root, parsed) => import("../github-cmd-D3L0f93X.mjs").then(({ runGithubCommand }) => runGithubCommand(root, parsed)));
1959
+ else if (command === "github") runSubcommandGroup("github", githubSubcommands, parseGithubArgs, (root, parsed) => import("../github-cmd-BvEKQmcD.mjs").then(({ runGithubCommand }) => runGithubCommand(root, parsed)));
1910
1960
  else if (command === "build") runSubcommandGroup("build", buildSubcommands, parseBuildArgs, (root, parsed) => import("../build-cmd-CJQSRupz.mjs").then(({ runBuildCommand }) => runBuildCommand(root, parsed)));
1911
1961
  else if (command === "prepare") {
1912
1962
  const root = process.cwd();
@@ -1979,7 +2029,7 @@ else if (command === "-v" || command === "--version") {
1979
2029
  },
1980
2030
  {
1981
2031
  name: "github",
1982
- hint: "install | installations | connect | update | link",
2032
+ hint: "install | installations | connect | update | status | link",
1983
2033
  description: "Connect a GitHub repository for automatic deploys"
1984
2034
  },
1985
2035
  {
@@ -2042,7 +2092,7 @@ else if (command === "-v" || command === "--version") {
2042
2092
  case "project": return runSubcommandGroup("project", projectSubcommands, parseProjectArgs, (r, p) => import("../project-cmd-InegJruy.mjs").then(({ runProjectCommand }) => runProjectCommand(r, p)));
2043
2093
  case "secret": return runSubcommandGroup("secret", secretSubcommands, parseSecretArgs, (r, p) => import("../secret-CVKwIOm3.mjs").then(({ runSecretCommand }) => runSecretCommand(r, p)));
2044
2094
  case "domain": return runSubcommandGroup("domain", domainSubcommands, parseDomainArgs, (r, p) => import("../domain-CkowysQx.mjs").then(({ runDomainCommand }) => runDomainCommand(r, p)));
2045
- case "github": return runSubcommandGroup("github", githubSubcommands, parseGithubArgs, (r, p) => import("../github-cmd-D3L0f93X.mjs").then(({ runGithubCommand }) => runGithubCommand(r, p)));
2095
+ case "github": return runSubcommandGroup("github", githubSubcommands, parseGithubArgs, (r, p) => import("../github-cmd-BvEKQmcD.mjs").then(({ runGithubCommand }) => runGithubCommand(r, p)));
2046
2096
  case "build": return runSubcommandGroup("build", buildSubcommands, parseBuildArgs, (r, p) => import("../build-cmd-CJQSRupz.mjs").then(({ runBuildCommand }) => runBuildCommand(r, p)));
2047
2097
  case "db": return runSubcommandGroup("db", dbSubcommands, parseDbArgs, (r, p) => import("../db-hAmBumot.mjs").then(({ runDbCommand }) => runDbCommand(r, p)));
2048
2098
  case "env": return runSubcommandGroup("env", envSubcommands, parseEnvArgs, (r, p) => import("../env-CIwG2y7G.mjs").then(({ runEnvCommand }) => runEnvCommand(r, p)));
@@ -7,6 +7,20 @@ import { t as collectGitDeployMetadata } from "./git-metadata-Ce0AtSZL.mjs";
7
7
  import { randomBytes } from "node:crypto";
8
8
  //#region src/cli/github-cmd.ts
9
9
  /**
10
+ * The workflow file `void init --github` scaffolds and the platform default for
11
+ * a connection's OIDC deploy pin. Kept in sync with the server default
12
+ * (`github_connections.workflow_path`) and the `init.ts` scaffolder.
13
+ */
14
+ const DEFAULT_WORKFLOW_PATH = ".github/workflows/void-deploy.yml";
15
+ /**
16
+ * Client-side mirror of the server's `isValidWorkflowPath` (packages/api
17
+ * routes/github.ts). Used to reject a bad path at the interactive prompt before
18
+ * an API round-trip. The server re-validates, so this is DX only, not security.
19
+ */
20
+ function isValidWorkflowPath(path) {
21
+ return path.length > 0 && path.startsWith(".github/workflows/") && (path.endsWith(".yml") || path.endsWith(".yaml")) && !path.includes("..");
22
+ }
23
+ /**
10
24
  * Map a PlatformApiError status + error code string to a friendly user message.
11
25
  * Returns null for unrecognised cases (caller should re-throw).
12
26
  */
@@ -62,6 +76,22 @@ function formatInstallations(installations) {
62
76
  return header;
63
77
  }).join("\n\n");
64
78
  }
79
+ /**
80
+ * Render a GitHub connection as an aligned key/value block for `void github
81
+ * status`. The workflow file is the OIDC deploy pin — it is only read for
82
+ * github_actions builds (container builds never use it), so it is annotated as
83
+ * unused when the executor is container to avoid implying it is active.
84
+ */
85
+ function formatConnection(connection) {
86
+ const row = (label, value) => ` ${label.padEnd(16)}${value}`;
87
+ const workflow = connection.build_executor === "github_actions" ? connection.workflow_path : `${connection.workflow_path} (unused for container builds)`;
88
+ return [
89
+ row("Repository", connection.repo_full_name),
90
+ row("Branch", connection.branch),
91
+ row("Build executor", connection.build_executor),
92
+ row("Deploy workflow", workflow)
93
+ ].join("\n");
94
+ }
65
95
  async function runGithubCommand(root, args) {
66
96
  const token = getToken(root);
67
97
  if (!token) {
@@ -74,6 +104,7 @@ async function runGithubCommand(root, args) {
74
104
  case "installations": return runInstallations(client);
75
105
  case "connect": return runConnect(root, client, args);
76
106
  case "update": return runGithubUpdate(root, client, args);
107
+ case "status": return runStatus(root, client, args);
77
108
  case "link": return runGithubLink(root, client);
78
109
  }
79
110
  }
@@ -294,7 +325,48 @@ async function runConnect(root, client, args) {
294
325
  }
295
326
  branch = input.trim() || branchDefault;
296
327
  }
297
- const executor = args.executor ?? "container";
328
+ let executor;
329
+ if (args.executor !== void 0) executor = args.executor;
330
+ else if (!isTTY) executor = "container";
331
+ else {
332
+ const picked = await Ee({
333
+ message: "Build executor:",
334
+ options: [{
335
+ value: "container",
336
+ label: "Cloudflare Container",
337
+ hint: "build on Void (default)"
338
+ }, {
339
+ value: "github_actions",
340
+ label: "GitHub Actions",
341
+ hint: "build in your repo's runner"
342
+ }],
343
+ initialValue: "container"
344
+ });
345
+ if (q(picked)) {
346
+ me("Cancelled.");
347
+ process.exit(0);
348
+ }
349
+ executor = picked;
350
+ }
351
+ let workflowPath = args.workflowPath;
352
+ if (executor === "github_actions" && args.workflowPath === void 0 && isTTY) {
353
+ const input = await Re({
354
+ message: "Deploy workflow file:",
355
+ placeholder: DEFAULT_WORKFLOW_PATH,
356
+ initialValue: DEFAULT_WORKFLOW_PATH,
357
+ validate(value) {
358
+ const v = (value ?? "").trim();
359
+ if (v.length === 0 || isValidWorkflowPath(v)) return;
360
+ return "Must be under .github/workflows/, end in .yml/.yaml, and not contain \"..\".";
361
+ }
362
+ });
363
+ if (q(input)) {
364
+ me("Cancelled.");
365
+ process.exit(0);
366
+ }
367
+ const chosen = input.trim() || ".github/workflows/void-deploy.yml";
368
+ if (chosen !== ".github/workflows/void-deploy.yml") workflowPath = chosen;
369
+ }
298
370
  let connection;
299
371
  try {
300
372
  connection = await client.connectGithub(projectId, {
@@ -302,7 +374,7 @@ async function runConnect(root, client, args) {
302
374
  repoFullName,
303
375
  branch,
304
376
  buildExecutor: executor,
305
- workflowPath: args.workflowPath
377
+ workflowPath
306
378
  });
307
379
  } catch (err) {
308
380
  if (err instanceof PlatformApiError) {
@@ -317,8 +389,8 @@ async function runConnect(root, client, args) {
317
389
  }
318
390
  throw err;
319
391
  }
320
- if (args.workflowPath !== void 0 && connection.workflow_path !== args.workflowPath) {
321
- R.error(`connect: server did not apply --workflow ${args.workflowPath} (got ${connection.workflow_path ?? "none"}). The platform may be too old to support a custom workflow path.`);
392
+ if (workflowPath !== void 0 && connection.workflow_path !== workflowPath) {
393
+ R.error(`connect: server did not apply workflow path ${workflowPath} (got ${connection.workflow_path ?? "none"}). The platform may be too old to support a custom workflow path.`);
322
394
  process.exit(1);
323
395
  }
324
396
  R.success(`Connected ${repoFullName} (${branch}) → project. Push to ${branch} to deploy.`);
@@ -381,6 +453,7 @@ async function runGithubUpdate(root, client, args) {
381
453
  }
382
454
  let branch;
383
455
  let executor;
456
+ let workflowPath = args.workflowPath ?? current.workflow_path;
384
457
  if (isTTY) {
385
458
  if (args.branch !== void 0) branch = args.branch;
386
459
  else {
@@ -414,6 +487,23 @@ async function runGithubUpdate(root, client, args) {
414
487
  }
415
488
  executor = picked;
416
489
  }
490
+ if (executor === "github_actions" && args.workflowPath === void 0) {
491
+ const input = await Re({
492
+ message: "Deploy workflow file:",
493
+ placeholder: current.workflow_path,
494
+ initialValue: current.workflow_path,
495
+ validate(value) {
496
+ const v = (value ?? "").trim();
497
+ if (v.length === 0 || isValidWorkflowPath(v)) return;
498
+ return "Must be under .github/workflows/, end in .yml/.yaml, and not contain \"..\".";
499
+ }
500
+ });
501
+ if (q(input)) {
502
+ me("Cancelled.");
503
+ process.exit(0);
504
+ }
505
+ workflowPath = input.trim() || current.workflow_path;
506
+ }
417
507
  } else {
418
508
  branch = args.branch ?? current.branch;
419
509
  executor = args.executor ?? current.build_executor;
@@ -421,7 +511,7 @@ async function runGithubUpdate(root, client, args) {
421
511
  const patch = {};
422
512
  if (branch !== current.branch) patch.branch = branch;
423
513
  if (executor !== current.build_executor) patch.buildExecutor = executor;
424
- if (args.workflowPath !== void 0 && args.workflowPath !== current.workflow_path) patch.workflowPath = args.workflowPath;
514
+ if (workflowPath !== current.workflow_path) patch.workflowPath = workflowPath;
425
515
  if (patch.branch === void 0 && patch.buildExecutor === void 0 && patch.workflowPath === void 0) {
426
516
  R.info(`No changes. ${current.repo_full_name} already deploys from ${current.branch} via ${current.build_executor}.`);
427
517
  return;
@@ -443,10 +533,70 @@ async function runGithubUpdate(root, client, args) {
443
533
  throw err;
444
534
  }
445
535
  if (patch.workflowPath !== void 0 && updated.workflow_path !== patch.workflowPath) {
446
- R.error(`update: server did not apply --workflow ${patch.workflowPath} (got ${updated.workflow_path ?? "none"}). The platform may be too old to support a custom workflow path.`);
536
+ R.error(`update: server did not apply workflow path ${patch.workflowPath} (got ${updated.workflow_path ?? "none"}). The platform may be too old to support a custom workflow path.`);
447
537
  process.exit(1);
448
538
  }
449
539
  R.success(`Updated ${updated.repo_full_name} → branch ${updated.branch}, executor ${updated.build_executor}, workflow ${updated.workflow_path}.`);
450
540
  }
541
+ /**
542
+ * `void github status [project]` — print the current GitHub connection (repo,
543
+ * branch, build executor, deploy workflow). Read-only; never mutates. Project
544
+ * resolution mirrors connect/update; a 404 means the project was never
545
+ * connected and points the user at `connect`.
546
+ */
547
+ async function runStatus(root, client, args) {
548
+ const isTTY = process.stdin.isTTY === true;
549
+ let projectId;
550
+ const requestedSlug = getRequestedProjectSlug({ projectSlug: args.projectName || void 0 });
551
+ if (requestedSlug) {
552
+ const resolved = resolveProjectBySlug(await client.listProjects(), requestedSlug);
553
+ if (!resolved) {
554
+ R.error(`status: No project found with slug '${requestedSlug}'.`);
555
+ process.exit(1);
556
+ }
557
+ projectId = resolved.projectId;
558
+ } else {
559
+ const linked = readProjectConfig(root);
560
+ if (linked) projectId = linked.projectId;
561
+ else if (!isTTY) {
562
+ R.error("status: No project specified. Pass a project name or --project <name>, set VOID_PROJECT, or link one with `void project link`.");
563
+ process.exit(1);
564
+ } else {
565
+ const projects = await client.listProjects();
566
+ if (projects.length === 0) {
567
+ R.error("No projects found. Create one first with `void deploy`.");
568
+ process.exit(1);
569
+ }
570
+ const picked = await Ee({
571
+ message: "Select a project:",
572
+ options: projects.map((p) => ({
573
+ value: p.id,
574
+ label: p.slug
575
+ }))
576
+ });
577
+ if (q(picked)) {
578
+ me("Cancelled.");
579
+ process.exit(0);
580
+ }
581
+ projectId = picked;
582
+ }
583
+ }
584
+ let connection;
585
+ try {
586
+ connection = await client.getGithubConnection(projectId);
587
+ } catch (err) {
588
+ if (err instanceof PlatformApiError) {
589
+ const body = parsePlatformErrorBody(err.body);
590
+ const code = body?.error ?? body?.message ?? "";
591
+ const message = describeUpdateError(err.status, code);
592
+ if (message !== null) {
593
+ R.error(`status: ${message}`);
594
+ process.exit(1);
595
+ }
596
+ }
597
+ throw err;
598
+ }
599
+ R.message(formatConnection(connection));
600
+ }
451
601
  //#endregion
452
602
  export { runGithubCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "void",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -353,7 +353,7 @@
353
353
  "valibot": ">=1.0.0-beta.7",
354
354
  "vite": "^8.0.0",
355
355
  "zod": "^3.25.0 || ^4.0.0",
356
- "@void/md": "0.10.3"
356
+ "@void/md": "0.10.4"
357
357
  },
358
358
  "peerDependenciesMeta": {
359
359
  "@void/md": {
@@ -618,6 +618,8 @@ Connect a GitHub repository to a Void project for automatic deploys. On every pu
618
618
 
619
619
  The `--workflow` path is the workflow file the GitHub OIDC exchange authorizes to mint this project's deploy token. It defaults to the scaffolded `.github/workflows/void-deploy.yml`; set it only if your deploy step lives in a different workflow file (it must be under `.github/workflows/` and end in `.yml`/`.yaml`). Because the exchange is secretless, **only this one workflow** can mint a deploy token — any other workflow in the repo (including an unsafe `pull_request_target`) is rejected. Pointing it at a broad, multi-purpose workflow widens that trust, so prefer a dedicated file.
620
620
 
621
+ Interactively (TTY), after resolving the repository and branch, `void github connect` prompts for the **build executor** (defaulting to `container`). If you pick `github_actions`, it then prompts for the **deploy workflow file**, defaulting to `.github/workflows/void-deploy.yml` and validated locally before it is sent. Pass `--executor` and/or `--workflow` to skip the respective prompt. The workflow prompt is skipped entirely for the `container` executor, which does not use a workflow file.
622
+
621
623
  **Non-interactive use (CI)**
622
624
 
623
625
  When stdin is not a TTY, `void github connect` never prompts — it fails closed and names any flag it needs. `--branch` is always required. `--project` must be resolvable (positional / `--project` / `VOID_PROJECT` / linked `.void/project.json`). `--installation` is required only when your account has more than one installation; otherwise the sole installation is used. `--repo` is required when the installation grants access to all repos or to more than one selected repo; when it grants exactly one repo that repo is used automatically. Use `void github installations` to discover the `installation_id`.
@@ -648,7 +650,7 @@ Update an existing connection's deploy **branch**, **build executor**, and/or au
648
650
  | `--executor <type>` | New build executor: `container` or `github_actions` |
649
651
  | `--workflow <path>` | New authorized deploy workflow file (under `.github/workflows/`) |
650
652
 
651
- Interactively (TTY), `void github update` shows the current branch and executor and prompts for new values, defaulting each to the current setting. `--workflow` is flag-only — it is never prompted for; pass it explicitly to change the authorized workflow file. When stdin is not a TTY, it never prompts: pass at least one of `--branch` / `--executor` / `--workflow`, or it fails closed. If nothing actually changes, the command reports "no changes" and does not call the API.
653
+ Interactively (TTY), `void github update` shows the current branch and executor and prompts for new values, defaulting each to the current setting. If the resulting executor is `github_actions`, it then prompts for the **deploy workflow file**, defaulting to the current path and validated locally; pass `--workflow` to skip that prompt. The workflow prompt is skipped when the executor is `container`, which does not use a workflow file. When stdin is not a TTY, it never prompts: pass at least one of `--branch` / `--executor` / `--workflow`, or it fails closed. If nothing actually changes, the command reports "no changes" and does not call the API.
652
654
 
653
655
  ```
654
656
  void github update my-app --executor github_actions
@@ -656,6 +658,26 @@ void github update my-app --executor github_actions
656
658
 
657
659
  **Project resolution** follows the same order as deploy: positional / `--project`, `VOID_PROJECT`, linked project (`.void/project.json`).
658
660
 
661
+ ### `void github status`
662
+
663
+ ```
664
+ void github status [project]
665
+ ```
666
+
667
+ Show a project's current GitHub connection: the connected **repository**, the deploy **branch**, the **build executor** (`container` or `github_actions`), and the authorized **deploy workflow file**. Read-only — it never changes anything. The workflow file is the OIDC pin used only for `github_actions` builds; on a `container` connection it is still shown but marked unused. The project must already be connected (run `void github connect` first, otherwise it reports that and exits).
668
+
669
+ **Options**
670
+
671
+ | Flag | Description |
672
+ | ------------------ | ------------------------------------------------ |
673
+ | `--project <name>` | Project name (alias for the positional argument) |
674
+
675
+ ```
676
+ void github status my-app
677
+ ```
678
+
679
+ **Project resolution** follows the same order as deploy: positional / `--project`, `VOID_PROJECT`, linked project (`.void/project.json`).
680
+
659
681
  ## Build
660
682
 
661
683
  Inspect Deploy-on-GitHub builds.