@apso/cli 0.29.1 → 0.30.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.
@@ -8,13 +8,14 @@ const lib_1 = require("../lib");
8
8
  const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
9
9
  const perf_hooks_1 = require("perf_hooks");
10
10
  const file_system_1 = require("../lib/utils/file-system");
11
+ const git_hooks_1 = require("../lib/utils/git-hooks");
11
12
  class Generate extends base_command_1.default {
12
13
  async run() {
13
14
  var _a, _b;
14
15
  const { flags } = await this.parse(Generate);
15
16
  const skipFormat = flags["skip-format"];
16
17
  const totalBuildStart = perf_hooks_1.performance.now();
17
- const { rootFolder, entities, relationshipMap, apiType, auth, emitEvents, http, language: configLanguage } = (0, lib_1.parseApsorc)();
18
+ const { rootFolder, entities, relationshipMap, apiType, auth, emitEvents, http, coAuthor, language: configLanguage } = (0, lib_1.parseApsorc)();
18
19
  // Resolve language: flag > .apsorc > prompt
19
20
  let language;
20
21
  if (flags.language) {
@@ -235,6 +236,24 @@ class Generate extends base_command_1.default {
235
236
  }
236
237
  const totalBuildTime = perf_hooks_1.performance.now() - totalBuildStart;
237
238
  console.log(`[apso] Finished building all entities in ${totalBuildTime.toFixed(2)} ms`);
239
+ // Best-effort: install the commit co-author hook so commits that include
240
+ // Apso-generated files credit Apso as a co-author. Never fails the command.
241
+ try {
242
+ const result = (0, git_hooks_1.installCoAuthorHook)(process.cwd(), {
243
+ disabled: coAuthor === false,
244
+ });
245
+ if (result.installed) {
246
+ console.log("[apso] Commit co-author hook installed (.apso/hooks)");
247
+ }
248
+ else if (result.reason === "custom-hookspath") {
249
+ console.log("[apso] A custom git core.hooksPath is already configured (e.g. husky), so the Apso co-author hook was not installed.\n" +
250
+ "[apso] To credit Apso on commits that include generated files, add this trailer to your existing prepare-commit-msg hook when the staged diff touches an `autogen/` path:\n" +
251
+ '[apso] git interpret-trailers --in-place --if-exists addIfDifferent --trailer "Co-authored-by: Apso <bot@apso.ai>" "$1"');
252
+ }
253
+ }
254
+ catch {
255
+ // Never let hook installation fail the generate command.
256
+ }
238
257
  }
239
258
  }
240
259
  Generate.description = "Generate code from .apsorc schema";
@@ -12,5 +12,11 @@ export default class Init extends BaseCommand {
12
12
  private cloneExisting;
13
13
  private createNew;
14
14
  private runOffline;
15
+ /**
16
+ * Best-effort install of the Apso commit co-author hook. Never throws, never
17
+ * fails the command. No `.apsorc` may exist yet at init time, so we only
18
+ * respect the `APSO_NO_COAUTHOR=1` env opt-out (handled inside the installer).
19
+ */
20
+ private installCoAuthorHook;
15
21
  private postSetup;
16
22
  }
@@ -13,6 +13,7 @@ const upgrade_1 = require("../lib/upgrade");
13
13
  const interactive_1 = require("../lib/utils/interactive");
14
14
  const config_2 = require("../lib/config");
15
15
  const template_1 = require("../lib/utils/template");
16
+ const git_hooks_1 = require("../lib/utils/git-hooks");
16
17
  class Init extends base_command_1.default {
17
18
  async run() {
18
19
  const { flags } = await this.parse(Init);
@@ -119,6 +120,7 @@ class Init extends base_command_1.default {
119
120
  linkedAt: new Date().toISOString(),
120
121
  };
121
122
  config_1.projectLink.write(link, projectPath);
123
+ this.installCoAuthorHook(projectPath);
122
124
  this.log(`\nProject cloned to ${projectPath}`);
123
125
  this.log(`Linked to workspace "${workspace.name}" / service "${service.name}"`);
124
126
  }
@@ -286,7 +288,29 @@ class Init extends base_command_1.default {
286
288
  this.log(`Language: ${language}`);
287
289
  this.log('Run "apso login" to link this project to the Apso platform.');
288
290
  }
291
+ /**
292
+ * Best-effort install of the Apso commit co-author hook. Never throws, never
293
+ * fails the command. No `.apsorc` may exist yet at init time, so we only
294
+ * respect the `APSO_NO_COAUTHOR=1` env opt-out (handled inside the installer).
295
+ */
296
+ installCoAuthorHook(projectPath) {
297
+ try {
298
+ const result = (0, git_hooks_1.installCoAuthorHook)(projectPath);
299
+ if (result.installed) {
300
+ this.log("[apso] Commit co-author hook installed (.apso/hooks)");
301
+ }
302
+ else if (result.reason === "custom-hookspath") {
303
+ this.log("[apso] A custom git core.hooksPath is already configured (e.g. husky), so the Apso co-author hook was not installed.\n" +
304
+ "[apso] To credit Apso on commits that include generated files, add this trailer to your existing prepare-commit-msg hook when the staged diff touches an `autogen/` path:\n" +
305
+ '[apso] git interpret-trailers --in-place --if-exists addIfDifferent --trailer "Co-authored-by: Apso <bot@apso.ai>" "$1"');
306
+ }
307
+ }
308
+ catch {
309
+ // Never let hook installation fail init.
310
+ }
311
+ }
289
312
  async postSetup(projectPath, language) {
313
+ this.installCoAuthorHook(projectPath);
290
314
  switch (language) {
291
315
  case "typescript": {
292
316
  if (shelljs_1.default.which("npm")) {
@@ -25,6 +25,12 @@ export type ApsorcType = {
25
25
  * gets a generated controller unless it opts back in with `http: true`.
26
26
  */
27
27
  http?: boolean;
28
+ /**
29
+ * Opt-out for the Apso commit co-author hook. When false, the CLI will not
30
+ * install the `prepare-commit-msg` hook that appends the Apso co-author
31
+ * trailer. Defaults to enabled.
32
+ */
33
+ coAuthor?: boolean;
28
34
  };
29
35
  type ParsedApsorcData = {
30
36
  entities: Entity[];
@@ -41,6 +47,8 @@ type ParsedApsorc = {
41
47
  emitEvents?: boolean;
42
48
  /** Top-level default for HTTP controller generation. */
43
49
  http?: boolean;
50
+ /** Opt-out for the Apso commit co-author hook (defaults to enabled). */
51
+ coAuthor?: boolean;
44
52
  };
45
53
  export declare const parseApsorcV1: (apsorc: ApsorcType) => ParsedApsorcData;
46
54
  export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;
@@ -38,6 +38,7 @@ const parseRc = () => {
38
38
  const language = apsoConfig.language;
39
39
  const emitEvents = apsoConfig.emitEvents;
40
40
  const http = apsoConfig.http;
41
+ const coAuthor = apsoConfig.coAuthor;
41
42
  return {
42
43
  rootFolder,
43
44
  apiType,
@@ -48,6 +49,7 @@ const parseRc = () => {
48
49
  language,
49
50
  emitEvents,
50
51
  http,
52
+ coAuthor,
51
53
  };
52
54
  };
53
55
  const parseApsorc = () => {
@@ -69,6 +71,7 @@ const parseApsorc = () => {
69
71
  language: apsoConfig.language,
70
72
  emitEvents: apsoConfig.emitEvents,
71
73
  http: apsoConfig.http,
74
+ coAuthor: apsoConfig.coAuthor,
72
75
  ...(0, exports.parseApsorcV1)(apsoConfig),
73
76
  };
74
77
  if (debug) {
@@ -87,6 +90,7 @@ const parseApsorc = () => {
87
90
  language: apsoConfig.language,
88
91
  emitEvents: apsoConfig.emitEvents,
89
92
  http: apsoConfig.http,
93
+ coAuthor: apsoConfig.coAuthor,
90
94
  ...(() => {
91
95
  const relStart = perf_hooks_1.performance.now();
92
96
  const parsed = (0, exports.parseApsorcV2)(apsoConfig);
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The Apso co-author git trailer. Appended to commits that include
3
+ * Apso-generated work so Apso is credited as a co-author.
4
+ */
5
+ export declare const APSO_COAUTHOR_TRAILER = "Co-authored-by: Apso <bot@apso.ai>";
6
+ /**
7
+ * Relative path (from the repo top-level) where the managed hook is installed.
8
+ * Modern git resolves a relative `core.hooksPath` from the repository root.
9
+ */
10
+ export declare const APSO_HOOKS_DIR = ".apso/hooks";
11
+ /**
12
+ * Builds the `prepare-commit-msg` bash script that appends the Apso co-author
13
+ * trailer to commits that touch Apso-generated paths.
14
+ *
15
+ * Pure function (no side effects) so it can be unit-tested directly.
16
+ *
17
+ * Behaviour of the generated script:
18
+ * - Skips merge/squash commits (`$2` is `merge` or `squash`).
19
+ * - Honors the `APSO_NO_COAUTHOR=1` opt-out.
20
+ * - Only acts when the staged changes include an Apso-generated path
21
+ * (a path segment named `autogen/`).
22
+ * - Adds the trailer idempotently via `git interpret-trailers`, which handles
23
+ * trailer-block formatting + dedupe and coexists with other `Co-authored-by`
24
+ * trailers (e.g. Claude).
25
+ */
26
+ export declare function buildCoAuthorHookScript(): string;
27
+ export interface InstallCoAuthorHookOptions {
28
+ /** When true, skip installation (e.g. `.apsorc` set `coAuthor: false`). */
29
+ disabled?: boolean;
30
+ }
31
+ export interface InstallCoAuthorHookResult {
32
+ installed: boolean;
33
+ reason?: "disabled" | "not-a-git-repo" | "custom-hookspath" | "error";
34
+ }
35
+ /**
36
+ * Installs the Apso co-author `prepare-commit-msg` hook into a project.
37
+ *
38
+ * Best-effort: never throws. Callers can log the result.
39
+ *
40
+ * - Respects the `disabled` option and the `APSO_NO_COAUTHOR=1` env opt-out.
41
+ * - No-ops (returns `not-a-git-repo`) outside a git work tree.
42
+ * - Writes `<projectRoot>/.apso/hooks/prepare-commit-msg` (mode 0755).
43
+ * - Sets the repo-local `core.hooksPath` to `.apso/hooks` when unset; leaves it
44
+ * alone (and reports success) when it's already `.apso/hooks`; refuses to
45
+ * clobber a custom `core.hooksPath` (husky etc.) and reports
46
+ * `custom-hookspath` so the caller can guide the user.
47
+ */
48
+ export declare function installCoAuthorHook(projectRoot: string, opts?: InstallCoAuthorHookOptions): InstallCoAuthorHookResult;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installCoAuthorHook = exports.buildCoAuthorHookScript = exports.APSO_HOOKS_DIR = exports.APSO_COAUTHOR_TRAILER = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ /**
9
+ * The Apso co-author git trailer. Appended to commits that include
10
+ * Apso-generated work so Apso is credited as a co-author.
11
+ */
12
+ exports.APSO_COAUTHOR_TRAILER = "Co-authored-by: Apso <bot@apso.ai>";
13
+ /**
14
+ * Relative path (from the repo top-level) where the managed hook is installed.
15
+ * Modern git resolves a relative `core.hooksPath` from the repository root.
16
+ */
17
+ exports.APSO_HOOKS_DIR = ".apso/hooks";
18
+ /**
19
+ * Builds the `prepare-commit-msg` bash script that appends the Apso co-author
20
+ * trailer to commits that touch Apso-generated paths.
21
+ *
22
+ * Pure function (no side effects) so it can be unit-tested directly.
23
+ *
24
+ * Behaviour of the generated script:
25
+ * - Skips merge/squash commits (`$2` is `merge` or `squash`).
26
+ * - Honors the `APSO_NO_COAUTHOR=1` opt-out.
27
+ * - Only acts when the staged changes include an Apso-generated path
28
+ * (a path segment named `autogen/`).
29
+ * - Adds the trailer idempotently via `git interpret-trailers`, which handles
30
+ * trailer-block formatting + dedupe and coexists with other `Co-authored-by`
31
+ * trailers (e.g. Claude).
32
+ */
33
+ function buildCoAuthorHookScript() {
34
+ return `#!/usr/bin/env bash
35
+ set -euo pipefail
36
+
37
+ # Managed by Apso (@apso/cli). Appends an Apso co-author trailer to commits
38
+ # that include Apso-generated files. To opt out, set APSO_NO_COAUTHOR=1 or
39
+ # remove this hook (and unset core.hooksPath).
40
+
41
+ COMMIT_MSG_FILE="\${1:-}"
42
+ COMMIT_SOURCE="\${2:-}"
43
+
44
+ # Don't trailer merge or squash commit messages.
45
+ if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ]; then
46
+ exit 0
47
+ fi
48
+
49
+ # Opt-out.
50
+ if [ "\${APSO_NO_COAUTHOR:-}" = "1" ]; then
51
+ exit 0
52
+ fi
53
+
54
+ # Only act when the staged changes include an Apso-generated path.
55
+ if ! git diff --cached --name-only | grep -qE '(^|/)autogen/'; then
56
+ exit 0
57
+ fi
58
+
59
+ # Add the trailer idempotently. git handles formatting + dedupe and coexists
60
+ # with an existing "Co-authored-by: Claude" trailer.
61
+ git interpret-trailers --in-place --if-exists addIfDifferent \\
62
+ --trailer "${exports.APSO_COAUTHOR_TRAILER}" "$COMMIT_MSG_FILE"
63
+ `;
64
+ }
65
+ exports.buildCoAuthorHookScript = buildCoAuthorHookScript;
66
+ /**
67
+ * Installs the Apso co-author `prepare-commit-msg` hook into a project.
68
+ *
69
+ * Best-effort: never throws. Callers can log the result.
70
+ *
71
+ * - Respects the `disabled` option and the `APSO_NO_COAUTHOR=1` env opt-out.
72
+ * - No-ops (returns `not-a-git-repo`) outside a git work tree.
73
+ * - Writes `<projectRoot>/.apso/hooks/prepare-commit-msg` (mode 0755).
74
+ * - Sets the repo-local `core.hooksPath` to `.apso/hooks` when unset; leaves it
75
+ * alone (and reports success) when it's already `.apso/hooks`; refuses to
76
+ * clobber a custom `core.hooksPath` (husky etc.) and reports
77
+ * `custom-hookspath` so the caller can guide the user.
78
+ */
79
+ function installCoAuthorHook(projectRoot, opts = {}) {
80
+ try {
81
+ if (opts.disabled || process.env.APSO_NO_COAUTHOR === "1") {
82
+ return { installed: false, reason: "disabled" };
83
+ }
84
+ // Verify we're inside a git work tree.
85
+ try {
86
+ (0, child_process_1.execFileSync)("git", ["-C", projectRoot, "rev-parse", "--is-inside-work-tree"], {
87
+ stdio: "ignore",
88
+ });
89
+ }
90
+ catch {
91
+ return { installed: false, reason: "not-a-git-repo" };
92
+ }
93
+ // Write the hook script.
94
+ const hooksDir = path.join(projectRoot, ".apso", "hooks");
95
+ fs.mkdirSync(hooksDir, { recursive: true });
96
+ const hookPath = path.join(hooksDir, "prepare-commit-msg");
97
+ fs.writeFileSync(hookPath, buildCoAuthorHookScript(), { mode: 0o755 });
98
+ // writeFileSync's mode is subject to umask on create and ignored on
99
+ // overwrite; chmod explicitly so the hook is always executable.
100
+ fs.chmodSync(hookPath, 0o755);
101
+ // Inspect the current core.hooksPath.
102
+ let currentHooksPath = "";
103
+ try {
104
+ currentHooksPath = (0, child_process_1.execFileSync)("git", ["-C", projectRoot, "config", "--get", "core.hooksPath"], { encoding: "utf8" }).trim();
105
+ }
106
+ catch {
107
+ // `git config --get` exits non-zero when the key is unset.
108
+ currentHooksPath = "";
109
+ }
110
+ if (currentHooksPath === "") {
111
+ (0, child_process_1.execFileSync)("git", ["-C", projectRoot, "config", "core.hooksPath", exports.APSO_HOOKS_DIR], { stdio: "ignore" });
112
+ return { installed: true };
113
+ }
114
+ if (currentHooksPath === exports.APSO_HOOKS_DIR) {
115
+ return { installed: true };
116
+ }
117
+ // Some other hooks path is configured (husky, etc.). Don't clobber it.
118
+ return { installed: false, reason: "custom-hookspath" };
119
+ }
120
+ catch {
121
+ return { installed: false, reason: "error" };
122
+ }
123
+ }
124
+ exports.installCoAuthorHook = installCoAuthorHook;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.29.1",
9
+ "version": "0.30.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@biomejs/biome": "^1.9.4",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.29.1",
2
+ "version": "0.30.0",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",