@decohere/devkit 0.0.1-dev.125 → 0.0.1-dev.5

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 (121) hide show
  1. package/package.json +3 -12
  2. package/src/cli/action.ts +77 -0
  3. package/src/cli/command.ts +173 -0
  4. package/src/cli/index.ts +30 -0
  5. package/src/cli/module.type.ts +48 -0
  6. package/src/cli/options.ts +81 -0
  7. package/src/diagnostics/trace.ts +92 -0
  8. package/src/interop/docker/docker.ts +57 -0
  9. package/src/interop/errors/interop.error.ts +1 -0
  10. package/src/interop/git/git.ts +98 -0
  11. package/{types/src/interop/git/git.types.d.ts → src/interop/git/git.types.ts} +9 -6
  12. package/{es6/src/interop/index.js → src/interop/index.ts} +1 -0
  13. package/src/interop/node/node.ts +38 -0
  14. package/src/interop/npm/npm.ts +168 -0
  15. package/src/interop/npm/npm.types.ts +28 -0
  16. package/src/interop/pnpm/pnpm.ts +22 -0
  17. package/{types/src/interop/pnpm/pnpm.types.d.ts → src/interop/pnpm/pnpm.types.ts} +1 -1
  18. package/src/interop/shell/shell.argv.ts +31 -0
  19. package/src/interop/shell/shell.ts +100 -0
  20. package/src/interop/shell/shell.types.ts +9 -0
  21. package/src/interop/types/detect-package-manager.ts +45 -0
  22. package/src/interop/types/package-manager.ts +7 -0
  23. package/src/interop/yarn/yarn.ts +28 -0
  24. package/{types/src/interop/yarn/yarn.types.d.ts → src/interop/yarn/yarn.types.ts} +1 -1
  25. package/src/io/find-directories.ts +13 -0
  26. package/src/io/find-file.ts +37 -0
  27. package/src/io/watch.ts +290 -0
  28. package/tests/cli/command.model.spec.ts +91 -0
  29. package/tests/diagnostics/trace.spec.ts +319 -0
  30. package/tests/interop/detect-package-manager.spec.ts +70 -0
  31. package/tests/interop/npm.spec.ts +129 -0
  32. package/tests/interop/pnpm.spec.ts +30 -0
  33. package/tests/interop/shell.spec.ts +47 -0
  34. package/tests/interop/yarn.spec.ts +18 -0
  35. package/tests/io/find-file.spec.ts +188 -0
  36. package/tests/io/watch.real-fs.spec.ts +76 -0
  37. package/tests/io/watch.spec.ts +291 -0
  38. package/LICENSE +0 -15
  39. package/commonjs/src/cli/action.js +0 -65
  40. package/commonjs/src/cli/command.js +0 -127
  41. package/commonjs/src/cli/index.js +0 -61
  42. package/commonjs/src/cli/module.type.js +0 -18
  43. package/commonjs/src/cli/options.js +0 -115
  44. package/commonjs/src/diagnostics/index.js +0 -22
  45. package/commonjs/src/diagnostics/trace.js +0 -79
  46. package/commonjs/src/interop/docker/docker.js +0 -54
  47. package/commonjs/src/interop/errors/interop.error.js +0 -12
  48. package/commonjs/src/interop/git/git.js +0 -134
  49. package/commonjs/src/interop/git/git.types.js +0 -4
  50. package/commonjs/src/interop/index.js +0 -97
  51. package/commonjs/src/interop/node/index.js +0 -18
  52. package/commonjs/src/interop/node/node.js +0 -45
  53. package/commonjs/src/interop/npm/npm.js +0 -231
  54. package/commonjs/src/interop/npm/npm.types.js +0 -16
  55. package/commonjs/src/interop/pnpm/pnpm.js +0 -34
  56. package/commonjs/src/interop/pnpm/pnpm.types.js +0 -4
  57. package/commonjs/src/interop/shell/shell.argv.js +0 -45
  58. package/commonjs/src/interop/shell/shell.js +0 -105
  59. package/commonjs/src/interop/shell/shell.types.js +0 -4
  60. package/commonjs/src/interop/types/detect-package-manager.js +0 -71
  61. package/commonjs/src/interop/types/package-manager.js +0 -4
  62. package/commonjs/src/interop/yarn/yarn.js +0 -34
  63. package/commonjs/src/interop/yarn/yarn.types.js +0 -4
  64. package/commonjs/src/io/find-directories.js +0 -21
  65. package/commonjs/src/io/find-file.js +0 -45
  66. package/commonjs/src/io/index.js +0 -20
  67. package/commonjs/src/io/watch.js +0 -261
  68. package/es6/src/cli/action.js +0 -47
  69. package/es6/src/cli/command.js +0 -106
  70. package/es6/src/cli/index.js +0 -4
  71. package/es6/src/cli/module.type.js +0 -11
  72. package/es6/src/cli/options.js +0 -76
  73. package/es6/src/diagnostics/trace.js +0 -61
  74. package/es6/src/interop/docker/docker.js +0 -44
  75. package/es6/src/interop/errors/interop.error.js +0 -2
  76. package/es6/src/interop/git/git.js +0 -124
  77. package/es6/src/interop/git/git.types.js +0 -1
  78. package/es6/src/interop/node/node.js +0 -35
  79. package/es6/src/interop/npm/npm.js +0 -180
  80. package/es6/src/interop/npm/npm.types.js +0 -6
  81. package/es6/src/interop/pnpm/pnpm.js +0 -24
  82. package/es6/src/interop/pnpm/pnpm.types.js +0 -1
  83. package/es6/src/interop/shell/shell.argv.js +0 -35
  84. package/es6/src/interop/shell/shell.js +0 -95
  85. package/es6/src/interop/shell/shell.types.js +0 -1
  86. package/es6/src/interop/types/detect-package-manager.js +0 -50
  87. package/es6/src/interop/types/package-manager.js +0 -1
  88. package/es6/src/interop/yarn/yarn.js +0 -24
  89. package/es6/src/interop/yarn/yarn.types.js +0 -1
  90. package/es6/src/io/find-directories.js +0 -11
  91. package/es6/src/io/find-file.js +0 -27
  92. package/es6/src/io/watch.js +0 -238
  93. package/types/src/cli/action.d.ts +0 -36
  94. package/types/src/cli/command.d.ts +0 -34
  95. package/types/src/cli/index.d.ts +0 -7
  96. package/types/src/cli/module.type.d.ts +0 -36
  97. package/types/src/cli/options.d.ts +0 -28
  98. package/types/src/diagnostics/index.d.ts +0 -1
  99. package/types/src/diagnostics/trace.d.ts +0 -10
  100. package/types/src/interop/docker/docker.d.ts +0 -19
  101. package/types/src/interop/errors/interop.error.d.ts +0 -2
  102. package/types/src/interop/git/git.d.ts +0 -16
  103. package/types/src/interop/index.d.ts +0 -14
  104. package/types/src/interop/node/index.d.ts +0 -1
  105. package/types/src/interop/node/node.d.ts +0 -5
  106. package/types/src/interop/npm/npm.d.ts +0 -29
  107. package/types/src/interop/npm/npm.types.d.ts +0 -24
  108. package/types/src/interop/pnpm/pnpm.d.ts +0 -9
  109. package/types/src/interop/shell/shell.argv.d.ts +0 -5
  110. package/types/src/interop/shell/shell.d.ts +0 -6
  111. package/types/src/interop/shell/shell.types.d.ts +0 -9
  112. package/types/src/interop/types/detect-package-manager.d.ts +0 -4
  113. package/types/src/interop/types/package-manager.d.ts +0 -10
  114. package/types/src/interop/yarn/yarn.d.ts +0 -9
  115. package/types/src/io/find-directories.d.ts +0 -2
  116. package/types/src/io/find-file.d.ts +0 -9
  117. package/types/src/io/index.d.ts +0 -3
  118. package/types/src/io/watch.d.ts +0 -16
  119. /package/{es6/src/diagnostics/index.js → src/diagnostics/index.ts} +0 -0
  120. /package/{es6/src/interop/node/index.js → src/interop/node/index.ts} +0 -0
  121. /package/{es6/src/io/index.js → src/io/index.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/devkit",
3
- "version": "0.0.1-dev.125",
3
+ "version": "0.0.1-dev.5",
4
4
  "description": "The shared base for Decohere's command-line tools: the command framework, git/npm/pnpm/docker wrappers, tracing, and file search and watch. Never needed in a production app.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -9,15 +9,6 @@
9
9
  "tooling",
10
10
  "decohere"
11
11
  ],
12
- "homepage": "https://github.com/typesmiths/decohere/tree/main/projects/devkit#readme",
13
- "bugs": {
14
- "url": "https://github.com/typesmiths/decohere/issues"
15
- },
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/typesmiths/decohere.git",
19
- "directory": "projects/devkit"
20
- },
21
12
  "license": "ISC",
22
13
  "author": "Typesmiths",
23
14
  "type": "module",
@@ -48,8 +39,8 @@
48
39
  "test": "echo \"Error: no test specified\" && exit 1"
49
40
  },
50
41
  "dependencies": {
51
- "@decohere/core": "0.0.1-dev.125",
52
- "@decohere/scribe": "0.0.1-dev.125",
42
+ "@decohere/core": "0.0.1-dev.5",
43
+ "@decohere/scribe": "0.0.1-dev.5",
53
44
  "commander": "^13.1.0",
54
45
  "glob": "^13.0.1",
55
46
  "picomatch": "^4.0.4"
@@ -0,0 +1,77 @@
1
+ import type { Constructable } from "@decohere/core/reflection";
2
+ import type { UnionToIntersection } from "@decohere/core/system";
3
+
4
+ export const ActionMap = Symbol("ActionMap");
5
+
6
+ export type ActionClassMap = Record<string, Constructable<Action>>;
7
+
8
+ export interface ResultStatus {
9
+ succeeded: boolean;
10
+ error?: string;
11
+ }
12
+
13
+ export interface Result {
14
+ readonly name: string;
15
+ readonly succeeded: boolean;
16
+ readonly skipped: boolean;
17
+ readonly message: string;
18
+ }
19
+
20
+ export class ActionResponse<TData = unknown> implements Result {
21
+ public static Success<TData>(name: string, data?: TData[]) {
22
+ return new ActionResponse<TData>({ name, data, succeeded: true });
23
+ }
24
+
25
+ public static Skipped(name: string) {
26
+ return new ActionResponse({ name, skipped: true });
27
+ }
28
+
29
+ public static Blocking(name: string) {
30
+ return new ActionResponse({ name, succeeded: true, blocking: true });
31
+ }
32
+
33
+ public static Error<TData = unknown>(name: string, message: string): ActionResponse<TData>;
34
+ public static Error<TData extends ResultStatus>(
35
+ name: string,
36
+ data: TData[],
37
+ ): ActionResponse<TData>;
38
+ public static Error<TData extends ResultStatus>(
39
+ name: string,
40
+ messageOrData: string | TData[],
41
+ data?: TData[],
42
+ ) {
43
+ if (Array.isArray(messageOrData)) {
44
+ const message = messageOrData
45
+ .filter((r) => !r.succeeded)
46
+ .map((r) => r.error ?? "Unknown error")
47
+ .join("\n");
48
+ return new ActionResponse<TData>({ name, message, data: messageOrData });
49
+ }
50
+ return new ActionResponse<TData>({ name, message: messageOrData, data });
51
+ }
52
+
53
+ public constructor(data: Partial<ActionResponse<TData>>) {
54
+ Object.assign(this, data);
55
+ }
56
+
57
+ public readonly name: string;
58
+ public readonly succeeded: boolean = false;
59
+ public readonly skipped: boolean = false;
60
+ public readonly blocking: boolean = false;
61
+ public readonly message: string;
62
+ public readonly data: TData[];
63
+ }
64
+
65
+ export interface Action<T extends object = object> {
66
+ run(options: T): Promise<ActionResponse>;
67
+ }
68
+
69
+ export type Actions = Record<string, Action>;
70
+
71
+ export type ActionOptions<A extends Action> = A extends Action<infer T> ? T : never;
72
+
73
+ export type MergeOptions<TActions extends Record<string, Action>> = UnionToIntersection<
74
+ {
75
+ [K in keyof TActions]: ActionOptions<TActions[K]>;
76
+ }[keyof TActions]
77
+ >;
@@ -0,0 +1,173 @@
1
+ import { Command as CommanderType, type OptionValues } from "commander";
2
+ import type { Constructable } from "@decohere/core/reflection";
3
+ import type { ActionResponse, Actions } from "./action.js";
4
+
5
+ export type CommandType = Constructable<DecohereCommand>;
6
+
7
+ export type CommandHandler = (opts: OptionValues, cmd: DecohereCommand) => Promise<void>;
8
+
9
+ export type CommandResponse<TActions extends Actions = Actions> = {
10
+ [ActionName in keyof TActions]?: ActionResponse;
11
+ };
12
+
13
+ export const CommandResponse = {
14
+ success<T extends Actions>(
15
+ _ctx: unknown,
16
+ responses: CommandResponse<T>,
17
+ ): CommandResponse<T> {
18
+ return responses;
19
+ },
20
+
21
+ help<T extends Actions = Actions>(): CommandResponse<T> {
22
+ return {} as CommandResponse<T>;
23
+ },
24
+
25
+ fromAction(name: string, response: ActionResponse): CommandResponse {
26
+ return { [name]: response };
27
+ },
28
+ };
29
+
30
+ /** Thrown when a command execution fails due to action errors. */
31
+ export class CommandError extends Error {}
32
+
33
+ export class DecohereCommand<TActions extends Actions = Actions> extends CommanderType {
34
+ public get actions(): readonly Constructable<import("./action.js").Action>[] {
35
+ return [];
36
+ }
37
+
38
+ public constructor(
39
+ name?: string | Constructable | Record<string, CommandType>,
40
+ handler?: CommandHandler,
41
+ ) {
42
+ super();
43
+ if (typeof name === "string" || typeof name === "function") {
44
+ this.name(this.parseCommandName(name));
45
+ } else {
46
+ this.name(this.parseCommandName(this.constructor as unknown as Constructable));
47
+ if (name) {
48
+ this.registerSubcommand(name, handler);
49
+ }
50
+ }
51
+ if (handler && !this.commands.length) {
52
+ this.action(handler);
53
+ }
54
+ this.exitOverride();
55
+ }
56
+
57
+ /**
58
+ * Whether the command, with these options, runs until it is stopped (`flux serve`,
59
+ * `forge build --watch`). The CLI then listens for SIGINT and SIGTERM from the moment it
60
+ * starts, so a stop during start-up still shuts down cleanly; other commands keep Node's
61
+ * default, where Ctrl+C ends them at once.
62
+ */
63
+ public runsUntilStopped(_options: OptionValues): boolean {
64
+ return false;
65
+ }
66
+
67
+ public async run(
68
+ actions: TActions,
69
+ options: OptionValues,
70
+ ): Promise<CommandResponse<TActions>> {
71
+ const responses = await this.runActions(actions, options);
72
+ this.throwOnErrors(responses);
73
+ return CommandResponse.success(options, responses);
74
+ }
75
+
76
+ public registerSubcommand(command: CommandType, onAction?: CommandHandler): void;
77
+ public registerSubcommand(
78
+ commands: Record<string, DecohereCommand | CommandType>,
79
+ onAction?: CommandHandler,
80
+ ): void;
81
+ public registerSubcommand(
82
+ commands: CommandType | Record<string, DecohereCommand | CommandType>,
83
+ onAction?: CommandHandler,
84
+ ): void {
85
+ if (typeof commands === "function") {
86
+ const instance = new commands(commands, onAction);
87
+ this.addCommand(instance);
88
+ if (onAction && !instance.commands.length) {
89
+ instance.action(onAction);
90
+ }
91
+ return;
92
+ }
93
+
94
+ for (const [key, command] of Object.entries(commands)) {
95
+ const instance =
96
+ command instanceof DecohereCommand ? command : new command(key, onAction);
97
+ this.addCommand(instance);
98
+ if (onAction && !instance.commands.length) {
99
+ instance.action(onAction);
100
+ }
101
+ }
102
+ }
103
+
104
+ protected async runActions(
105
+ actions: TActions,
106
+ options: OptionValues,
107
+ ): Promise<CommandResponse<TActions>> {
108
+ const selectedActions = this.getSelectedActions(actions);
109
+ const responses = {} as CommandResponse<TActions>;
110
+ for (const [actionKey, action] of selectedActions) {
111
+ responses[actionKey] = (await action.run(
112
+ options,
113
+ )) as CommandResponse<TActions>[keyof TActions];
114
+ }
115
+ return responses as CommandResponse<TActions>;
116
+ }
117
+
118
+ protected getSelectedActions(
119
+ actions: TActions,
120
+ ): Array<[keyof TActions, TActions[keyof TActions]]> {
121
+ if (!this.actions.length) {
122
+ return [];
123
+ }
124
+
125
+ const actionEntries = Object.entries(actions) as Array<
126
+ [keyof TActions, TActions[keyof TActions]]
127
+ >;
128
+ return this.actions.map((ActionClass) => {
129
+ const selectedAction = actionEntries.find(([, action]) => action instanceof ActionClass);
130
+ if (!selectedAction) {
131
+ throw new CommandError(
132
+ `${this.name()} command missing action instance for ${ActionClass.name}`,
133
+ );
134
+ }
135
+
136
+ return selectedAction;
137
+ });
138
+ }
139
+
140
+ protected throwOnErrors(responses: CommandResponse<TActions>): void {
141
+ const failures = Object.entries(responses).filter(([, response]) => {
142
+ if (!response) {
143
+ return false;
144
+ }
145
+
146
+ return !response.succeeded && !response.skipped;
147
+ });
148
+
149
+ if (!failures.length) {
150
+ return;
151
+ }
152
+
153
+ throw new CommandError(
154
+ `${this.name()} command failed. ${failures
155
+ .map(
156
+ ([actionName, response]) => `${actionName}: ${response?.message ?? "Unknown error"}`,
157
+ )
158
+ .join("\n")}`,
159
+ );
160
+ }
161
+
162
+ private parseCommandName(stringLike: string | Constructable) {
163
+ const command =
164
+ typeof stringLike === "string"
165
+ ? stringLike
166
+ : (stringLike.name?.toLowerCase().replace("command", "") ?? "");
167
+
168
+ if (command === "") {
169
+ throw new CommandError(`Could not parse command name from ${stringLike.toString()}`);
170
+ }
171
+ return command;
172
+ }
173
+ }
@@ -0,0 +1,30 @@
1
+ export type {
2
+ Action,
3
+ ActionClassMap,
4
+ ActionOptions,
5
+ Actions,
6
+ MergeOptions,
7
+ Result,
8
+ ResultStatus,
9
+ } from "./action.js";
10
+ export { ActionMap, ActionResponse } from "./action.js";
11
+ export type { CommandHandler, CommandType } from "./command.js";
12
+ export { CommandError, CommandResponse, DecohereCommand } from "./command.js";
13
+ export type {
14
+ ApplicationModule,
15
+ CommandMap as CommandClassMap,
16
+ ModuleConfig,
17
+ WorkspaceFactoryOptions,
18
+ } from "./module.type.js";
19
+ export { ModuleManifest } from "./module.type.js";
20
+ export {
21
+ ConfigFileOption,
22
+ ConfigurationOption,
23
+ DryRunOption,
24
+ EnvFileIfExistsOption,
25
+ EnvFileOption,
26
+ LogLevelOption,
27
+ NoBuildOptions,
28
+ NoCleanOptions,
29
+ ProjectOption,
30
+ } from "./options.js";
@@ -0,0 +1,48 @@
1
+ import type { ContainerBuilder } from "@decohere/core/composition";
2
+ import type { ActionClassMap } from "./action.js";
3
+ import type { CommandType, DecohereCommand } from "./command.js";
4
+
5
+ export type CommandMap = Record<string, CommandType>;
6
+
7
+ export type WorkspaceFactoryOptions = {
8
+ configFile?: string;
9
+ rootDir?: string;
10
+ };
11
+
12
+ export interface ApplicationModule {
13
+ id: string;
14
+ dispose?(): void;
15
+ asyncDispose?(): Promise<void>;
16
+ command: DecohereCommand;
17
+ }
18
+
19
+ export type ModuleConfig = {
20
+ actions?: ActionClassMap;
21
+ commands?: CommandMap;
22
+ };
23
+
24
+ /**
25
+ * Simple declaration interface for a CLI module.
26
+ * Extend this and implement the four abstract members.
27
+ */
28
+ export abstract class ModuleManifest {
29
+ /** Unique module identifier, used as the root command name. */
30
+ public abstract readonly id: string;
31
+
32
+ /** Sub-command classes this module contributes. */
33
+ public readonly commands: CommandMap = {};
34
+
35
+ /** Action classes this module contributes. */
36
+ public readonly actions: ActionClassMap = {};
37
+
38
+ /** The root command class for this module. */
39
+ public abstract readonly command: CommandType;
40
+
41
+ /** Register infrastructure services into the DI container. */
42
+ public registerServices(_builder: ContainerBuilder): void {}
43
+
44
+ /** Lazily create a workspace instance. Return null if this module does not use a workspace. */
45
+ public createWorkspace(_options: WorkspaceFactoryOptions): Promise<object | null> {
46
+ return Promise.resolve(null);
47
+ }
48
+ }
@@ -0,0 +1,81 @@
1
+ import { Option } from "commander";
2
+
3
+ function parseOptionalBoolean(input: string | boolean): boolean {
4
+ if (typeof input === "boolean") {
5
+ return input;
6
+ }
7
+
8
+ const value = input.trim().toLowerCase();
9
+ if (["true", "1", "yes", "on"].includes(value)) {
10
+ return true;
11
+ }
12
+ if (["false", "0", "no", "off"].includes(value)) {
13
+ return false;
14
+ }
15
+
16
+ throw new Error(`Invalid boolean value: "${input}". Use true/false.`);
17
+ }
18
+
19
+ export class ConfigurationOption extends Option {
20
+ public constructor(defaultValue: string) {
21
+ super("--configuration <configuration>", "Configuration name");
22
+ this.default(defaultValue);
23
+ }
24
+ }
25
+
26
+ export class ConfigFileOption extends Option {
27
+ public constructor(defaultValue = ".decohere.js") {
28
+ super("--config-file <file>", "Path to Decohere config file");
29
+ this.default(defaultValue);
30
+ }
31
+ }
32
+
33
+ export class LogLevelOption extends Option {
34
+ public constructor() {
35
+ super("--log-level <level>", "Log level (trace, debug, info, warn, error)");
36
+ }
37
+ }
38
+
39
+ export class EnvFileOption extends Option {
40
+ public constructor() {
41
+ super("--env-file <file>", "Load environment variables from this file before running");
42
+ }
43
+ }
44
+
45
+ export class EnvFileIfExistsOption extends Option {
46
+ public constructor() {
47
+ super(
48
+ "--env-file-if-exists <file>",
49
+ "Like --env-file, but skip it when the file is missing",
50
+ );
51
+ }
52
+ }
53
+
54
+ export class NoCleanOptions extends Option {
55
+ public constructor(defaultValue: boolean) {
56
+ super("--no-clean", "Will not clean the project directories");
57
+ this.default(defaultValue);
58
+ }
59
+ }
60
+
61
+ export class NoBuildOptions extends Option {
62
+ public constructor(defaultValue: boolean) {
63
+ super("--no-build", "Will not run the build step");
64
+ this.default(defaultValue);
65
+ }
66
+ }
67
+
68
+ export class DryRunOption extends Option {
69
+ public constructor(description: string, defaultValue = false) {
70
+ super("--dry-run [dryRun]", description);
71
+ this.default(defaultValue);
72
+ this.preset(true);
73
+ this.argParser(parseOptionalBoolean);
74
+ }
75
+ }
76
+
77
+ export class ProjectOption extends Option {
78
+ public constructor() {
79
+ super("--project <project>", "Project name to target");
80
+ }
81
+ }
@@ -0,0 +1,92 @@
1
+ import { isPromise } from "node:util/types";
2
+ import { scribe } from "@decohere/scribe";
3
+
4
+ /**
5
+ * Trace mode is toggled with FORGE_TRACE, read when a method is decorated. Raising the log level
6
+ * to see the output is the CLI's job at its entry point: importing this module changes nothing.
7
+ */
8
+ function isTraceEnabled(): boolean {
9
+ return process.env.FORGE_TRACE != null;
10
+ }
11
+
12
+ type AnyFn = (...args: unknown[]) => unknown;
13
+ type TimingLevel = "info" | "trace";
14
+
15
+ function serializeArgs(args: unknown[]): string {
16
+ if (args.length === 0) return "";
17
+ return args
18
+ .map((arg) => {
19
+ if (Array.isArray(arg)) return `[${arg.map(String).join(", ")}]`;
20
+ const s = String(arg);
21
+ return s.length > 60 ? `${s.slice(0, 57)}...` : s;
22
+ })
23
+ .join(", ");
24
+ }
25
+
26
+ function wrapWithTiming(
27
+ fn: AnyFn,
28
+ className: string,
29
+ methodName: string,
30
+ logLevel: TimingLevel,
31
+ ): AnyFn {
32
+ let logger: ReturnType<typeof scribe.logger> | undefined;
33
+ return function (this: unknown, ...args: unknown[]) {
34
+ if (logger == null) {
35
+ logger = scribe.logger(className);
36
+ }
37
+ const log = logger;
38
+ const label = `${methodName}(${serializeArgs(args)})`;
39
+ const start = performance.now();
40
+ const logDuration = () => {
41
+ log[logLevel](`${label}: ${(performance.now() - start).toFixed(2)}ms`);
42
+ };
43
+ const result = fn.apply(this, args);
44
+ if (isPromise(result)) {
45
+ return result.finally(logDuration);
46
+ }
47
+ logDuration();
48
+ return result;
49
+ };
50
+ }
51
+
52
+ export function trace(
53
+ target: object,
54
+ propertyKey: string,
55
+ descriptor: PropertyDescriptor,
56
+ ): PropertyDescriptor {
57
+ if (!isTraceEnabled()) return descriptor;
58
+ const className = (target as { constructor: { name: string } }).constructor.name;
59
+ descriptor.value = wrapWithTiming(
60
+ descriptor.value as AnyFn,
61
+ className,
62
+ propertyKey,
63
+ "trace",
64
+ );
65
+ return descriptor;
66
+ }
67
+
68
+ /**
69
+ * Always-on method timing. Logs duration at `info` regardless of `FORGE_TRACE`.
70
+ * Use this to time a specific method without raising the process log level.
71
+ */
72
+ export function timed(
73
+ target: object,
74
+ propertyKey: string,
75
+ descriptor: PropertyDescriptor,
76
+ ): PropertyDescriptor {
77
+ const className = (target as { constructor: { name: string } }).constructor.name;
78
+ descriptor.value = wrapWithTiming(descriptor.value as AnyFn, className, propertyKey, "info");
79
+ return descriptor;
80
+ }
81
+
82
+ export function Trace(constructor: { name: string; prototype: object }): void {
83
+ if (!isTraceEnabled()) return;
84
+ const className = constructor.name;
85
+ for (const key of Object.getOwnPropertyNames(constructor.prototype)) {
86
+ if (key === "constructor") continue;
87
+ const descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, key);
88
+ if (descriptor == null || typeof descriptor.value !== "function") continue;
89
+ descriptor.value = wrapWithTiming(descriptor.value as AnyFn, className, key, "trace");
90
+ Object.defineProperty(constructor.prototype, key, descriptor);
91
+ }
92
+ }
@@ -0,0 +1,57 @@
1
+ import { InteropError } from "../errors/interop.error.js";
2
+ import { Shell } from "../shell/shell.js";
3
+
4
+ export type DockerBuildOptions = {
5
+ cwd?: string;
6
+ dockerfile?: string;
7
+ tags?: string[];
8
+ buildArgs?: Record<string, string>;
9
+ secrets?: Record<string, string>;
10
+ noCache?: boolean;
11
+ pull?: boolean;
12
+ };
13
+
14
+ export type DockerPushOptions = {
15
+ cwd?: string;
16
+ };
17
+
18
+ export class Docker extends Shell {
19
+ public readonly name = "docker" as const;
20
+ protected program = "docker";
21
+
22
+ public async build(contextDir: string, options: DockerBuildOptions = {}) {
23
+ if (contextDir == null || contextDir.trim() === "") {
24
+ throw new InteropError("Docker build context is required");
25
+ }
26
+
27
+ const args = ["build"];
28
+ if (options.dockerfile) {
29
+ args.push("-f", options.dockerfile);
30
+ }
31
+ for (const tag of options.tags ?? []) {
32
+ args.push("-t", tag);
33
+ }
34
+ for (const [key, value] of Object.entries(options.buildArgs ?? {})) {
35
+ args.push("--build-arg", `${key}=${value}`);
36
+ }
37
+ for (const [id, env] of Object.entries(options.secrets ?? {})) {
38
+ args.push("--secret", `id=${id},env=${env}`);
39
+ }
40
+ if (options.noCache) {
41
+ args.push("--no-cache");
42
+ }
43
+ if (options.pull) {
44
+ args.push("--pull");
45
+ }
46
+ args.push(contextDir);
47
+ return await this.run({ cwd: options.cwd }, ...args);
48
+ }
49
+
50
+ public async push(tag: string, options: DockerPushOptions = {}) {
51
+ if (tag == null || tag.trim() === "") {
52
+ throw new InteropError("Docker tag is required");
53
+ }
54
+
55
+ return await this.run({ cwd: options.cwd }, "push", tag);
56
+ }
57
+ }
@@ -0,0 +1 @@
1
+ export class InteropError extends Error {}
@@ -0,0 +1,98 @@
1
+ import { InteropError } from "../errors/interop.error.js";
2
+ import { Shell } from "../shell/shell.js";
3
+ import type { ShellResponse } from "../shell/shell.types.js";
4
+ import type { CommitOptions, GitOptions, PushOptions, StageOptions } from "./git.types.js";
5
+
6
+ const Commands = {
7
+ stage: "stage",
8
+ tag: "tag",
9
+ add: "add",
10
+ commit: "commit",
11
+ push: "push",
12
+ listFiles: "ls-files",
13
+ };
14
+
15
+ const defaultOptions: GitOptions = { cwd: "./" };
16
+
17
+ export class Git extends Shell {
18
+ protected program: string = "git";
19
+ public async getModifiedFiles(options: GitOptions = defaultOptions) {
20
+ const args = [Commands.listFiles, "--modified"];
21
+ const { cwd } = options;
22
+ const result = await this.run({ cwd }, ...args);
23
+ return result.data;
24
+ }
25
+
26
+ public async getBranchName(options: GitOptions = defaultOptions) {
27
+ const { cwd } = options;
28
+ const args = ["rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD"];
29
+ const result = await this.run({ cwd }, ...args);
30
+ return result.data[0];
31
+ }
32
+
33
+ public async tag(tag: string, options: GitOptions = defaultOptions) {
34
+ const args = [Commands.tag, tag];
35
+ const { cwd } = options;
36
+ const result = await this.run({ cwd }, ...args);
37
+ return result;
38
+ }
39
+
40
+ public async pushTag(
41
+ tag: string,
42
+ options: GitOptions & { remote?: string } = defaultOptions,
43
+ ) {
44
+ const { cwd, remote = "origin" } = options;
45
+ const result = await this.run({ cwd }, Commands.push, remote, tag);
46
+ return result;
47
+ }
48
+
49
+ public async commit(message: string, options: CommitOptions): Promise<ShellResponse> {
50
+ const optionsAndDefaults = { ...defaultOptions, ...options };
51
+ const { cwd, files = [], all } = optionsAndDefaults;
52
+ const args = [Commands.commit];
53
+
54
+ if (files.length > 0 && all) {
55
+ throw new InteropError("Cannot commit all and files at the same time");
56
+ }
57
+
58
+ if (files.length > 0) {
59
+ await this.stage({ files });
60
+ }
61
+
62
+ if (options.all) {
63
+ args.push("-a");
64
+ }
65
+
66
+ args.push("-m", message);
67
+ const result = await this.run({ cwd }, ...args);
68
+ return result;
69
+ }
70
+
71
+ public async push(branch: string, options?: PushOptions) {
72
+ const optionsAndDefaults = { ...defaultOptions, ...options };
73
+ const { cwd, setUpstream } = optionsAndDefaults;
74
+ const args = [Commands.push];
75
+ args.push("--set-upstream", setUpstream ?? "origin");
76
+ args.push(branch);
77
+ const result = await this.run({ cwd }, ...args);
78
+ return result;
79
+ }
80
+
81
+ public async pull(branch: string, options: GitOptions = defaultOptions) {
82
+ const { cwd } = options;
83
+ return this.run({ cwd }, "pull", "--rebase", "origin", branch);
84
+ }
85
+
86
+ public async stage(options: StageOptions) {
87
+ const optionsAndDefaults = { ...defaultOptions, ...options };
88
+ const { cwd, files = [], all } = optionsAndDefaults;
89
+ if (all) {
90
+ await this.run({ cwd }, Commands.stage, "-a");
91
+ return;
92
+ }
93
+
94
+ for (const file of files) {
95
+ await this.run({ cwd }, Commands.stage, file);
96
+ }
97
+ }
98
+ }