@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.
- package/dist/commands/generate.js +20 -1
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +24 -0
- package/dist/lib/apsorc-parser.d.ts +8 -0
- package/dist/lib/apsorc-parser.js +4 -0
- package/dist/lib/utils/git-hooks.d.ts +48 -0
- package/dist/lib/utils/git-hooks.js +124 -0
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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";
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
9
|
+
"version": "0.30.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@biomejs/biome": "^1.9.4",
|
package/oclif.manifest.json
CHANGED