@polymorphism-tech/morph-spec 10.2.0 → 10.5.3
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/README.md +2 -2
- package/bin/morph-spec.js +9 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +1 -1
- package/framework/docs/CLI.md +22 -2
- package/framework/evals/review-checklist.md +5 -5
- package/framework/hooks/claude-code/core/heavy-gate.js +21 -2
- package/framework/hooks/shared/background-spawn.js +20 -0
- package/framework/hooks/shared/graph-refresh.js +2 -1
- package/framework/hooks/shared/heavy-config.js +21 -0
- package/framework/hooks/shared/host-services.js +9 -6
- package/framework/hooks/shared/next-step.js +11 -7
- package/framework/rules/plan-grammar.md +4 -1
- package/framework/schemas/config.schema.json +39 -0
- package/framework/schemas/feature.schema.json +14 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +7 -4
- package/framework/skills/core/morph-apply/references/machine.md +3 -2
- package/framework/skills/core/morph-apply/references/review.md +5 -3
- package/framework/skills/core/morph-plan/SKILL.md +4 -3
- package/framework/skills/core/morph-plan/references/critique.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +2 -1
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/project/heavy.js +13 -0
- package/src/commands/validation/verify.js +51 -5
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/heavy/gate.js +34 -2
- package/src/lib/heavy/reclaim.js +107 -0
- package/src/lib/heavy/watchdog.js +46 -0
- package/src/lib/next-step.js +1 -0
- package/src/lib/plan/checks.js +1 -1
- package/src/lib/procs/prune.js +23 -1
- package/src/lib/tasks/test-nodes.js +5 -0
- package/src/lib/validators/verify-runner.js +34 -4
- package/src/lib/verify/affected.js +141 -0
- package/src/lib/verify/detached.js +156 -0
- package/src/utils/hooks-installer.js +8 -0
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
|
+
import { backgroundArgs } from '../../../framework/hooks/shared/background-spawn.js';
|
|
7
|
+
import { heavyDir } from '../../../framework/hooks/shared/heavy-holders.js';
|
|
8
|
+
import { pidAlive } from '../../../framework/hooks/shared/process-alive.js';
|
|
9
|
+
import { treeFingerprint } from '../git/tree-fingerprint.js';
|
|
10
|
+
|
|
11
|
+
export const DETACH_STAGE_ENV = 'MORPH_VERIFY_STAGE';
|
|
12
|
+
export const DETACH_RUN_ENV = 'MORPH_VERIFY_RUN';
|
|
13
|
+
export const DETACH_WORKER_ENV = 'MORPH_VERIFY_WORKER_PID';
|
|
14
|
+
|
|
15
|
+
const POLL_MS = 2_000;
|
|
16
|
+
const MAX_RUN_MS = 3 * 60 * 60_000;
|
|
17
|
+
|
|
18
|
+
export const EXIT_BUSY = 3;
|
|
19
|
+
|
|
20
|
+
export function runPaths(cwd, feature, env = process.env) {
|
|
21
|
+
const id = createHash('sha1').update(`${cwd}\0${feature}`).digest('hex').slice(0, 12);
|
|
22
|
+
const base = join(heavyDir(env), `verify-${id}`);
|
|
23
|
+
return { run: `${base}.json`, exit: `${base}.exit`, out: `${base}.out`, err: `${base}.err` };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const readJson = (path) => {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const readExit = (path) => {
|
|
35
|
+
try {
|
|
36
|
+
return Number.parseInt(readFileSync(path, 'utf8'), 10);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const readText = (path) => {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(path, 'utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const removeRun = (paths) => {
|
|
51
|
+
for (const file of Object.values(paths)) rmSync(file, { force: true });
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function matchesRun(record, { key, args }) {
|
|
55
|
+
return record?.key === key && JSON.stringify(record.args) === JSON.stringify(args);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isRunning(record, paths, { isAlive = pidAlive, now = Date.now } = {}) {
|
|
59
|
+
if (!record || existsSync(paths.exit)) return false;
|
|
60
|
+
return isAlive(record.pid) && now() - record.startedAt < MAX_RUN_MS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const runFilesOf = (runFile) => ({
|
|
64
|
+
run: runFile,
|
|
65
|
+
exit: runFile.replace(/\.json$/, '.exit'),
|
|
66
|
+
out: runFile.replace(/\.json$/, '.out'),
|
|
67
|
+
err: runFile.replace(/\.json$/, '.err'),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function spawnStage(stage, { argv, cwd, env, paths, script, spawnFn = spawn }) {
|
|
71
|
+
mkdirSync(join(paths.run, '..'), { recursive: true });
|
|
72
|
+
const out = openSync(paths.out, 'a');
|
|
73
|
+
const err = openSync(paths.err, 'a');
|
|
74
|
+
try {
|
|
75
|
+
const args = [script, ...argv];
|
|
76
|
+
const child = spawnFn(process.execPath, stage === 'launch' ? args : backgroundArgs(args), {
|
|
77
|
+
cwd,
|
|
78
|
+
detached: true,
|
|
79
|
+
windowsHide: true,
|
|
80
|
+
stdio: ['ignore', out, err],
|
|
81
|
+
env: { ...env, [DETACH_STAGE_ENV]: stage, [DETACH_RUN_ENV]: paths.run },
|
|
82
|
+
});
|
|
83
|
+
child.unref();
|
|
84
|
+
return child;
|
|
85
|
+
} finally {
|
|
86
|
+
closeSync(out);
|
|
87
|
+
closeSync(err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function markWorkerExit(runFile, { processRef = process } = {}) {
|
|
92
|
+
const exitFile = runFilesOf(runFile).exit;
|
|
93
|
+
processRef.once('exit', (code) => {
|
|
94
|
+
try {
|
|
95
|
+
writeFileSync(exitFile, String(code ?? 1));
|
|
96
|
+
} catch {}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The launcher exists to die: killing a process tree walks parent pids, so a worker whose
|
|
102
|
+
* parent has exited is out of reach of whatever kills the session's background shell.
|
|
103
|
+
*/
|
|
104
|
+
export function launchWorker({ env = process.env, cwd = process.cwd(), argv, script = process.argv[1], spawnFn }) {
|
|
105
|
+
const runFile = env[DETACH_RUN_ENV];
|
|
106
|
+
const paths = runFilesOf(runFile);
|
|
107
|
+
const child = spawnStage('worker', { argv, cwd, env, paths, script, spawnFn });
|
|
108
|
+
writeFileSync(runFile, JSON.stringify({ ...readJson(runFile), pid: child.pid }));
|
|
109
|
+
spawnStage('watchdog', {
|
|
110
|
+
argv, cwd, env: { ...env, [DETACH_WORKER_ENV]: String(child.pid) }, paths, script, spawnFn,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function superviseVerify({
|
|
115
|
+
feature, args, cwd = process.cwd(), env = process.env, script = process.argv[1], log = (m) => process.stderr.write(`${m}\n`),
|
|
116
|
+
deps = {},
|
|
117
|
+
}) {
|
|
118
|
+
const {
|
|
119
|
+
fingerprint = treeFingerprint, spawnFn, isAlive = pidAlive, now = Date.now, wait = sleep, pollMs = POLL_MS,
|
|
120
|
+
} = deps;
|
|
121
|
+
const paths = runPaths(cwd, feature, env);
|
|
122
|
+
const key = fingerprint(cwd);
|
|
123
|
+
let record = readJson(paths.run);
|
|
124
|
+
const liveness = { isAlive, now };
|
|
125
|
+
|
|
126
|
+
if (isRunning(record, paths, liveness) && !matchesRun(record, { key, args })) {
|
|
127
|
+
log('verify: outra rodada desta feature ainda corre, sobre outra árvore; espero ela acabar');
|
|
128
|
+
while (isRunning(record, paths, liveness)) await wait(pollMs);
|
|
129
|
+
record = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const attachable = matchesRun(record, { key, args }) && (isRunning(record, paths, liveness) || existsSync(paths.exit));
|
|
133
|
+
if (attachable) {
|
|
134
|
+
log(`verify: retomo a rodada destacada iniciada às ${new Date(record.startedAt).toLocaleTimeString()}`);
|
|
135
|
+
} else {
|
|
136
|
+
removeRun(paths);
|
|
137
|
+
mkdirSync(join(paths.run, '..'), { recursive: true });
|
|
138
|
+
record = { pid: null, key, args, startedAt: now() };
|
|
139
|
+
writeFileSync(paths.run, JSON.stringify(record));
|
|
140
|
+
spawnStage('launch', { argv: args, cwd, env, paths, script, spawnFn });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
while (!existsSync(paths.exit)) {
|
|
144
|
+
const current = readJson(paths.run);
|
|
145
|
+
const launching = !current?.pid && now() - record.startedAt < 30_000;
|
|
146
|
+
if (!launching && !isRunning(current, paths, liveness)) break;
|
|
147
|
+
await wait(pollMs);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const exitCode = readExit(paths.exit);
|
|
151
|
+
const stdout = readText(paths.out);
|
|
152
|
+
const stderr = readText(paths.err);
|
|
153
|
+
removeRun(paths);
|
|
154
|
+
const died = exitCode === null || (exitCode === EXIT_BUSY && !stdout.trim());
|
|
155
|
+
return { exitCode: exitCode ?? EXIT_BUSY, stdout, stderr, died };
|
|
156
|
+
}
|
|
@@ -3,6 +3,7 @@ import { readFile } from 'fs/promises';
|
|
|
3
3
|
import { existsSync, readdirSync } from 'fs';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { realEffects } from '../lib/installers/effects.js';
|
|
6
|
+
import { CPU_SHARE_ENVS, cpuShareOf, readHeavyConfig } from '../../framework/hooks/shared/heavy-config.js';
|
|
6
7
|
|
|
7
8
|
const HOOKS_VERSION = '3.17.0';
|
|
8
9
|
|
|
@@ -275,6 +276,13 @@ export async function installClaudeHooks(targetPath, { fx = realEffects() } = {}
|
|
|
275
276
|
settings.plansDirectory = '.morph/plans';
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
// What runs outside the queue (a loose `dotnet test`) keeps to a run's share too; a value already here stays.
|
|
280
|
+
const heavy = readHeavyConfig(targetPath);
|
|
281
|
+
if (heavy.yieldToUser) {
|
|
282
|
+
const share = String(cpuShareOf(heavy.slots));
|
|
283
|
+
settings.env = { ...Object.fromEntries(CPU_SHARE_ENVS.map((name) => [name, share])), ...settings.env };
|
|
284
|
+
}
|
|
285
|
+
|
|
278
286
|
settings._morph_managed = settings._morph_managed || {};
|
|
279
287
|
settings._morph_managed.hooks_version = HOOKS_VERSION;
|
|
280
288
|
settings._morph_managed.hooks_installed = [...new Set(MORPH_HOOKS.map(h => h.event))];
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
<templateSet group="MORPH Templates">
|
|
2
|
-
<template name="morph-pascal" value="{{pascalCase $FEATURE_NAME$}}" description="Convert to PascalCase (UserAuth)" toReformat="false" toShortenFQNames="true">
|
|
3
|
-
<variable name="FEATURE_NAME" expression="" defaultValue=""FEATURE_NAME"" alwaysStopAt="true" />
|
|
4
|
-
<context>
|
|
5
|
-
<option name="OTHER" value="true" />
|
|
6
|
-
</context>
|
|
7
|
-
</template>
|
|
8
|
-
|
|
9
|
-
<template name="morph-camel" value="{{camelCase $FEATURE_NAME$}}" description="Convert to camelCase (userAuth)" toReformat="false" toShortenFQNames="true">
|
|
10
|
-
<variable name="FEATURE_NAME" expression="" defaultValue=""FEATURE_NAME"" alwaysStopAt="true" />
|
|
11
|
-
<context>
|
|
12
|
-
<option name="OTHER" value="true" />
|
|
13
|
-
</context>
|
|
14
|
-
</template>
|
|
15
|
-
|
|
16
|
-
<template name="morph-snake" value="{{snakeCase $FEATURE_NAME$}}" description="Convert to snake_case (user_auth)" toReformat="false" toShortenFQNames="true">
|
|
17
|
-
<variable name="FEATURE_NAME" expression="" defaultValue=""FEATURE_NAME"" alwaysStopAt="true" />
|
|
18
|
-
<context>
|
|
19
|
-
<option name="OTHER" value="true" />
|
|
20
|
-
</context>
|
|
21
|
-
</template>
|
|
22
|
-
|
|
23
|
-
<template name="morph-title" value="{{titleCase $FEATURE_NAME$}}" description="Convert to Title Case (User Auth)" toReformat="false" toShortenFQNames="true">
|
|
24
|
-
<variable name="FEATURE_NAME" expression="" defaultValue=""FEATURE_NAME"" alwaysStopAt="true" />
|
|
25
|
-
<context>
|
|
26
|
-
<option name="OTHER" value="true" />
|
|
27
|
-
</context>
|
|
28
|
-
</template>
|
|
29
|
-
|
|
30
|
-
<template name="morph-kebab" value="{{kebabCase $FEATURE_NAME$}}" description="Convert to kebab-case (user-auth)" toReformat="false" toShortenFQNames="true">
|
|
31
|
-
<variable name="FEATURE_NAME" expression="" defaultValue=""FEATURE_NAME"" alwaysStopAt="true" />
|
|
32
|
-
<context>
|
|
33
|
-
<option name="OTHER" value="true" />
|
|
34
|
-
</context>
|
|
35
|
-
</template>
|
|
36
|
-
|
|
37
|
-
<template name="morph-if" value="{{#if $CONDITION$}} $END$ {{/if}}" description="Handlebars if block" toReformat="false" toShortenFQNames="true">
|
|
38
|
-
<variable name="CONDITION" expression="" defaultValue=""condition"" alwaysStopAt="true" />
|
|
39
|
-
<context>
|
|
40
|
-
<option name="OTHER" value="true" />
|
|
41
|
-
</context>
|
|
42
|
-
</template>
|
|
43
|
-
|
|
44
|
-
<template name="morph-if-else" value="{{#if $CONDITION$}} $TRUE_BRANCH$ {{else}} $FALSE_BRANCH$ {{/if}}" description="Handlebars if-else block" toReformat="false" toShortenFQNames="true">
|
|
45
|
-
<variable name="CONDITION" expression="" defaultValue=""condition"" alwaysStopAt="true" />
|
|
46
|
-
<variable name="TRUE_BRANCH" expression="" defaultValue="""" alwaysStopAt="true" />
|
|
47
|
-
<variable name="FALSE_BRANCH" expression="" defaultValue="""" alwaysStopAt="true" />
|
|
48
|
-
<context>
|
|
49
|
-
<option name="OTHER" value="true" />
|
|
50
|
-
</context>
|
|
51
|
-
</template>
|
|
52
|
-
|
|
53
|
-
<template name="morph-each" value="{{#each $ITEMS$}} {{this.$PROPERTY$}} {{/each}}" description="Handlebars each loop" toReformat="false" toShortenFQNames="true">
|
|
54
|
-
<variable name="ITEMS" expression="" defaultValue=""items"" alwaysStopAt="true" />
|
|
55
|
-
<variable name="PROPERTY" expression="" defaultValue=""name"" alwaysStopAt="true" />
|
|
56
|
-
<context>
|
|
57
|
-
<option name="OTHER" value="true" />
|
|
58
|
-
</context>
|
|
59
|
-
</template>
|
|
60
|
-
|
|
61
|
-
<template name="morph-service" value="namespace {{NAMESPACE}}.Application.Features.{{pascalCase FEATURE_NAME}}.Services; public class {{pascalCase FEATURE_NAME}}Service( I{{pascalCase FEATURE_NAME}}Repository repository, ILogger<{{pascalCase FEATURE_NAME}}Service> logger) : I{{pascalCase FEATURE_NAME}}Service { public async Task<{{pascalCase FEATURE_NAME}}Dto?> GetByIdAsync(int id, CancellationToken cancellationToken = default) { $END$ } }" description="MORPH .NET Service class template" toReformat="false" toShortenFQNames="true">
|
|
62
|
-
<context>
|
|
63
|
-
<option name="C_SHARP" value="true" />
|
|
64
|
-
</context>
|
|
65
|
-
</template>
|
|
66
|
-
|
|
67
|
-
<template name="morph-repository" value="public class {{pascalCase FEATURE_NAME}}Repository(AppDbContext context) : I{{pascalCase FEATURE_NAME}}Repository { private readonly DbSet<{{pascalCase FEATURE_NAME}}> _dbSet = context.Set<{{pascalCase FEATURE_NAME}}>(); public async Task<{{pascalCase FEATURE_NAME}}?> GetByIdAsync(int id, CancellationToken cancellationToken = default) { $END$ } }" description="MORPH .NET Repository class template" toReformat="false" toShortenFQNames="true">
|
|
68
|
-
<context>
|
|
69
|
-
<option name="C_SHARP" value="true" />
|
|
70
|
-
</context>
|
|
71
|
-
</template>
|
|
72
|
-
|
|
73
|
-
<template name="morph-component" value="@page "/{{kebabCase FEATURE_NAME}}" @inject I{{pascalCase FEATURE_NAME}}Service {{pascalCase FEATURE_NAME}}Service @inject ILogger<{{pascalCase FEATURE_NAME}}Page> Logger <PageTitle>{{pascalCase FEATURE_NAME}}</PageTitle> <div class="container-fluid"> $END$ </div> @code { }" description="MORPH Blazor component template" toReformat="false" toShortenFQNames="true">
|
|
74
|
-
<context>
|
|
75
|
-
<option name="HTML" value="true" />
|
|
76
|
-
</context>
|
|
77
|
-
</template>
|
|
78
|
-
|
|
79
|
-
<template name="morph-migration" value="public partial class Add{{pascalCase FEATURE_NAME}} : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "{{pascalCase FEATURE_NAME}}s", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), $END$ }, constraints: table => { table.PrimaryKey("PK_{{pascalCase FEATURE_NAME}}s", x => x.Id); }); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable(name: "{{pascalCase FEATURE_NAME}}s"); } }" description="MORPH EF Core migration template" toReformat="false" toShortenFQNames="true">
|
|
80
|
-
<context>
|
|
81
|
-
<option name="C_SHARP" value="true" />
|
|
82
|
-
</context>
|
|
83
|
-
</template>
|
|
84
|
-
|
|
85
|
-
<template name="morph-ts-contract" value="export interface {{pascalCase FEATURE_NAME}} { id: number; $FIELD1$ } export interface Create{{pascalCase FEATURE_NAME}}Request { $FIELD2$ } export interface Update{{pascalCase FEATURE_NAME}}Request { $END$ }" description="MORPH TypeScript contract template" toReformat="false" toShortenFQNames="true">
|
|
86
|
-
<variable name="FIELD1" expression="" defaultValue="""" alwaysStopAt="true" />
|
|
87
|
-
<variable name="FIELD2" expression="" defaultValue="""" alwaysStopAt="true" />
|
|
88
|
-
<context>
|
|
89
|
-
<option name="TypeScript" value="true" />
|
|
90
|
-
</context>
|
|
91
|
-
</template>
|
|
92
|
-
</templateSet>
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"MORPH Template - PascalCase": {
|
|
3
|
-
"prefix": "morph-pascal",
|
|
4
|
-
"body": ["{{pascalCase ${1:FEATURE_NAME}}}"],
|
|
5
|
-
"description": "Convert to PascalCase (UserAuth)"
|
|
6
|
-
},
|
|
7
|
-
"MORPH Template - camelCase": {
|
|
8
|
-
"prefix": "morph-camel",
|
|
9
|
-
"body": ["{{camelCase ${1:FEATURE_NAME}}}"],
|
|
10
|
-
"description": "Convert to camelCase (userAuth)"
|
|
11
|
-
},
|
|
12
|
-
"MORPH Template - snake_case": {
|
|
13
|
-
"prefix": "morph-snake",
|
|
14
|
-
"body": ["{{snakeCase ${1:FEATURE_NAME}}}"],
|
|
15
|
-
"description": "Convert to snake_case (user_auth)"
|
|
16
|
-
},
|
|
17
|
-
"MORPH Template - Title Case": {
|
|
18
|
-
"prefix": "morph-title",
|
|
19
|
-
"body": ["{{titleCase ${1:FEATURE_NAME}}}"],
|
|
20
|
-
"description": "Convert to Title Case (User Auth)"
|
|
21
|
-
},
|
|
22
|
-
"MORPH Template - kebab-case": {
|
|
23
|
-
"prefix": "morph-kebab",
|
|
24
|
-
"body": ["{{kebabCase ${1:FEATURE_NAME}}}"],
|
|
25
|
-
"description": "Convert to kebab-case (user-auth)"
|
|
26
|
-
},
|
|
27
|
-
"MORPH Template - UPPER_SNAKE_CASE": {
|
|
28
|
-
"prefix": "morph-upper-snake",
|
|
29
|
-
"body": ["{{upperSnakeCase ${1:FEATURE_NAME}}}"],
|
|
30
|
-
"description": "Convert to UPPER_SNAKE_CASE (USER_AUTH)"
|
|
31
|
-
},
|
|
32
|
-
"MORPH Template - Conditional If": {
|
|
33
|
-
"prefix": "morph-if",
|
|
34
|
-
"body": [
|
|
35
|
-
"{{#if ${1:condition}}}",
|
|
36
|
-
" $2",
|
|
37
|
-
"{{/if}}"
|
|
38
|
-
],
|
|
39
|
-
"description": "Handlebars if block"
|
|
40
|
-
},
|
|
41
|
-
"MORPH Template - Conditional If-Else": {
|
|
42
|
-
"prefix": "morph-if-else",
|
|
43
|
-
"body": [
|
|
44
|
-
"{{#if ${1:condition}}}",
|
|
45
|
-
" $2",
|
|
46
|
-
"{{else}}",
|
|
47
|
-
" $3",
|
|
48
|
-
"{{/if}}"
|
|
49
|
-
],
|
|
50
|
-
"description": "Handlebars if-else block"
|
|
51
|
-
},
|
|
52
|
-
"MORPH Template - Each Loop": {
|
|
53
|
-
"prefix": "morph-each",
|
|
54
|
-
"body": [
|
|
55
|
-
"{{#each ${1:items}}}",
|
|
56
|
-
" {{this.$2}}",
|
|
57
|
-
"{{/each}}"
|
|
58
|
-
],
|
|
59
|
-
"description": "Handlebars each loop"
|
|
60
|
-
},
|
|
61
|
-
"MORPH Template - Unless": {
|
|
62
|
-
"prefix": "morph-unless",
|
|
63
|
-
"body": [
|
|
64
|
-
"{{#unless ${1:condition}}}",
|
|
65
|
-
" $2",
|
|
66
|
-
"{{/unless}}"
|
|
67
|
-
],
|
|
68
|
-
"description": "Handlebars unless block"
|
|
69
|
-
},
|
|
70
|
-
"MORPH Template - .NET Service Class": {
|
|
71
|
-
"prefix": "morph-service",
|
|
72
|
-
"body": [
|
|
73
|
-
"namespace {{NAMESPACE}}.Application.Features.{{pascalCase FEATURE_NAME}}.Services;",
|
|
74
|
-
"",
|
|
75
|
-
"public class {{pascalCase FEATURE_NAME}}Service(",
|
|
76
|
-
" I{{pascalCase FEATURE_NAME}}Repository repository,",
|
|
77
|
-
" ILogger<{{pascalCase FEATURE_NAME}}Service> logger) : I{{pascalCase FEATURE_NAME}}Service",
|
|
78
|
-
"{",
|
|
79
|
-
" public async Task<{{pascalCase FEATURE_NAME}}Dto?> GetByIdAsync(int id, CancellationToken cancellationToken = default)",
|
|
80
|
-
" {",
|
|
81
|
-
" $1",
|
|
82
|
-
" }",
|
|
83
|
-
"}"
|
|
84
|
-
],
|
|
85
|
-
"description": "MORPH .NET Service class template"
|
|
86
|
-
},
|
|
87
|
-
"MORPH Template - .NET Repository Class": {
|
|
88
|
-
"prefix": "morph-repository",
|
|
89
|
-
"body": [
|
|
90
|
-
"public class {{pascalCase FEATURE_NAME}}Repository(AppDbContext context) : I{{pascalCase FEATURE_NAME}}Repository",
|
|
91
|
-
"{",
|
|
92
|
-
" private readonly DbSet<{{pascalCase FEATURE_NAME}}> _dbSet = context.Set<{{pascalCase FEATURE_NAME}}>();",
|
|
93
|
-
"",
|
|
94
|
-
" public async Task<{{pascalCase FEATURE_NAME}}?> GetByIdAsync(int id, CancellationToken cancellationToken = default)",
|
|
95
|
-
" {",
|
|
96
|
-
" $1",
|
|
97
|
-
" }",
|
|
98
|
-
"}"
|
|
99
|
-
],
|
|
100
|
-
"description": "MORPH .NET Repository class template"
|
|
101
|
-
},
|
|
102
|
-
"MORPH Template - Blazor Component": {
|
|
103
|
-
"prefix": "morph-component",
|
|
104
|
-
"body": [
|
|
105
|
-
"@page \"/{{kebabCase FEATURE_NAME}}\"",
|
|
106
|
-
"@inject I{{pascalCase FEATURE_NAME}}Service {{pascalCase FEATURE_NAME}}Service",
|
|
107
|
-
"@inject ILogger<{{pascalCase FEATURE_NAME}}Page> Logger",
|
|
108
|
-
"",
|
|
109
|
-
"<PageTitle>{{pascalCase FEATURE_NAME}}</PageTitle>",
|
|
110
|
-
"",
|
|
111
|
-
"<div class=\"container-fluid\">",
|
|
112
|
-
" $1",
|
|
113
|
-
"</div>",
|
|
114
|
-
"",
|
|
115
|
-
"@code {",
|
|
116
|
-
" $2",
|
|
117
|
-
"}"
|
|
118
|
-
],
|
|
119
|
-
"description": "MORPH Blazor component template"
|
|
120
|
-
},
|
|
121
|
-
"MORPH Template - EF Core Migration": {
|
|
122
|
-
"prefix": "morph-migration",
|
|
123
|
-
"body": [
|
|
124
|
-
"public partial class Add{{pascalCase FEATURE_NAME}} : Migration",
|
|
125
|
-
"{",
|
|
126
|
-
" protected override void Up(MigrationBuilder migrationBuilder)",
|
|
127
|
-
" {",
|
|
128
|
-
" migrationBuilder.CreateTable(",
|
|
129
|
-
" name: \"{{pascalCase FEATURE_NAME}}s\",",
|
|
130
|
-
" columns: table => new",
|
|
131
|
-
" {",
|
|
132
|
-
" Id = table.Column<int>(type: \"int\", nullable: false)",
|
|
133
|
-
" .Annotation(\"SqlServer:Identity\", \"1, 1\"),",
|
|
134
|
-
" $1",
|
|
135
|
-
" },",
|
|
136
|
-
" constraints: table =>",
|
|
137
|
-
" {",
|
|
138
|
-
" table.PrimaryKey(\"PK_{{pascalCase FEATURE_NAME}}s\", x => x.Id);",
|
|
139
|
-
" });",
|
|
140
|
-
" }",
|
|
141
|
-
"",
|
|
142
|
-
" protected override void Down(MigrationBuilder migrationBuilder)",
|
|
143
|
-
" {",
|
|
144
|
-
" migrationBuilder.DropTable(name: \"{{pascalCase FEATURE_NAME}}s\");",
|
|
145
|
-
" }",
|
|
146
|
-
"}"
|
|
147
|
-
],
|
|
148
|
-
"description": "MORPH EF Core migration template"
|
|
149
|
-
},
|
|
150
|
-
"MORPH Template - Hangfire Job": {
|
|
151
|
-
"prefix": "morph-job",
|
|
152
|
-
"body": [
|
|
153
|
-
"public class {{pascalCase FEATURE_NAME}}ProcessorJob(",
|
|
154
|
-
" I{{pascalCase FEATURE_NAME}}Service service,",
|
|
155
|
-
" ILogger<{{pascalCase FEATURE_NAME}}ProcessorJob> logger) : I{{pascalCase FEATURE_NAME}}ProcessorJob",
|
|
156
|
-
"{",
|
|
157
|
-
" [AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]",
|
|
158
|
-
" [Queue(\"default\")]",
|
|
159
|
-
" [JobDisplayName(\"{{pascalCase FEATURE_NAME}} Processing - ID: {0}\")]",
|
|
160
|
-
" public async Task ExecuteAsync(int id, CancellationToken cancellationToken)",
|
|
161
|
-
" {",
|
|
162
|
-
" $1",
|
|
163
|
-
" }",
|
|
164
|
-
"}"
|
|
165
|
-
],
|
|
166
|
-
"description": "MORPH Hangfire job template"
|
|
167
|
-
},
|
|
168
|
-
"MORPH Template - TypeScript Contract": {
|
|
169
|
-
"prefix": "morph-ts-contract",
|
|
170
|
-
"body": [
|
|
171
|
-
"export interface {{pascalCase FEATURE_NAME}} {",
|
|
172
|
-
" id: number;",
|
|
173
|
-
" $1",
|
|
174
|
-
"}",
|
|
175
|
-
"",
|
|
176
|
-
"export interface Create{{pascalCase FEATURE_NAME}}Request {",
|
|
177
|
-
" $2",
|
|
178
|
-
"}",
|
|
179
|
-
"",
|
|
180
|
-
"export interface Update{{pascalCase FEATURE_NAME}}Request {",
|
|
181
|
-
" $3",
|
|
182
|
-
"}"
|
|
183
|
-
],
|
|
184
|
-
"description": "MORPH TypeScript contract template"
|
|
185
|
-
}
|
|
186
|
-
}
|