@mcptoolshop/style-dataset-lab 2.2.1 → 3.0.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/CHANGELOG.md +125 -0
- package/README.es.md +76 -144
- package/README.fr.md +75 -143
- package/README.hi.md +75 -143
- package/README.it.md +76 -144
- package/README.ja.md +75 -143
- package/README.md +36 -8
- package/README.pt-BR.md +75 -143
- package/README.zh.md +76 -144
- package/SECURITY.md +3 -1
- package/bin/sdlab.js +146 -17
- package/lib/adapters/comfyui-runner.js +170 -0
- package/lib/adapters/comfyui-workflows.js +163 -0
- package/lib/batch-compiler.js +118 -0
- package/lib/batch-modes.js +111 -0
- package/lib/batch-runs.js +168 -0
- package/lib/batch-sheet-render.js +206 -0
- package/lib/brief-compiler.js +598 -0
- package/lib/brief-render.js +205 -0
- package/lib/critique-engine.js +413 -0
- package/lib/critique-render.js +171 -0
- package/lib/errors.js +82 -0
- package/lib/generated-provenance.js +87 -0
- package/lib/log.js +36 -0
- package/lib/paths.js +31 -0
- package/lib/refine-briefs.js +329 -0
- package/lib/reingest-selected.js +173 -0
- package/lib/run-summary.js +131 -0
- package/lib/runtime-runs.js +168 -0
- package/lib/selections.js +323 -0
- package/lib/snapshot.js +5 -2
- package/lib/split.js +5 -2
- package/lib/workflow-profiles.js +161 -0
- package/package.json +8 -6
- package/scripts/batch-generate.js +167 -0
- package/scripts/batch-sheet.js +46 -0
- package/scripts/batch-show.js +86 -0
- package/scripts/brief-compile.js +134 -0
- package/scripts/brief-show.js +52 -0
- package/scripts/critique-show.js +50 -0
- package/scripts/critique.js +70 -0
- package/scripts/doctor.js +169 -1
- package/scripts/init.js +22 -0
- package/scripts/refine.js +119 -0
- package/scripts/reingest-selected.js +78 -0
- package/scripts/reingest.js +7 -1
- package/scripts/run-generate.js +76 -0
- package/scripts/run-list.js +58 -0
- package/scripts/run-show.js +63 -0
- package/scripts/select.js +118 -0
- package/scripts/selection-show.js +88 -0
- package/scripts/snapshot.js +27 -23
- package/scripts/split.js +3 -1
- package/scripts/workflow-list.js +61 -0
- package/scripts/workflow-show.js +96 -0
- package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
- package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
- package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
- package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
- package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
- package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
- package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
- package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
- package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
- package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Critique renderer — converts critique report to human-readable formats.
|
|
3
|
+
*
|
|
4
|
+
* Reads like a working note, not a report card.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── Fit badge ───────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const FIT_BADGES = {
|
|
10
|
+
strong: '✓ strong',
|
|
11
|
+
usable: '○ usable',
|
|
12
|
+
'off-model': '✗ off-model',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const FIT_EMOJI = {
|
|
16
|
+
strong: '🟢',
|
|
17
|
+
usable: '🟡',
|
|
18
|
+
'off-model': '🔴',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ─── Markdown renderer ──────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render a critique report as Markdown.
|
|
25
|
+
*/
|
|
26
|
+
export function renderCritiqueMarkdown(report) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
|
|
29
|
+
lines.push(`# Critique: ${report.run_id}`);
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push(`**Brief:** ${report.brief_id}`);
|
|
32
|
+
lines.push(`**Workflow:** ${report.workflow_id}`);
|
|
33
|
+
lines.push(`**Project:** ${report.project_id}`);
|
|
34
|
+
if (report.subject_id) lines.push(`**Subject:** ${report.subject_id}`);
|
|
35
|
+
lines.push(`**Created:** ${report.created_at}`);
|
|
36
|
+
lines.push('');
|
|
37
|
+
|
|
38
|
+
// Recommended action
|
|
39
|
+
const action = report.recommended_action;
|
|
40
|
+
const modeLabel = {
|
|
41
|
+
accept_one: '✓ Accept one candidate',
|
|
42
|
+
refine_from_one: '→ Refine from one candidate',
|
|
43
|
+
rerun_broader: '↻ Rerun with broader parameters',
|
|
44
|
+
discard_run: '✗ Discard this run',
|
|
45
|
+
};
|
|
46
|
+
lines.push('## Recommended Action');
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(`**${modeLabel[action.mode] || action.mode}**`);
|
|
49
|
+
if (action.preferred_candidate) {
|
|
50
|
+
lines.push(`Preferred: **${action.preferred_candidate}**`);
|
|
51
|
+
}
|
|
52
|
+
if (action.reason) lines.push(`Reason: ${action.reason}`);
|
|
53
|
+
lines.push('');
|
|
54
|
+
|
|
55
|
+
// Active dimensions
|
|
56
|
+
lines.push('## Active Dimensions');
|
|
57
|
+
lines.push('');
|
|
58
|
+
for (const dim of report.active_dimensions) {
|
|
59
|
+
lines.push(`- ${dim.replace(/_/g, ' ')}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push('');
|
|
62
|
+
|
|
63
|
+
// Candidates
|
|
64
|
+
lines.push('## Candidates');
|
|
65
|
+
lines.push('');
|
|
66
|
+
|
|
67
|
+
for (const c of report.candidates) {
|
|
68
|
+
const badge = FIT_EMOJI[c.overall_fit] || '?';
|
|
69
|
+
lines.push(`### ${badge} ${c.filename} (seed: ${c.seed ?? '?'})`);
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push(`**Fit:** ${c.overall_fit}`);
|
|
72
|
+
lines.push('');
|
|
73
|
+
|
|
74
|
+
if (c.strengths.length > 0) {
|
|
75
|
+
lines.push('**Strengths:**');
|
|
76
|
+
for (const s of c.strengths) lines.push(`- ${s}`);
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (c.drift_issues.length > 0) {
|
|
81
|
+
lines.push('**Drift Issues:**');
|
|
82
|
+
for (const d of c.drift_issues) lines.push(`- ⚠ ${d}`);
|
|
83
|
+
lines.push('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (c.preserve_next_pass.length > 0) {
|
|
87
|
+
lines.push('**Preserve Next Pass:**');
|
|
88
|
+
for (const p of c.preserve_next_pass) lines.push(`- ✓ ${p}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (c.correct_next_pass.length > 0) {
|
|
93
|
+
lines.push('**Correct Next Pass:**');
|
|
94
|
+
for (const x of c.correct_next_pass) lines.push(`- → ${x}`);
|
|
95
|
+
lines.push('');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Next-pass direction summary
|
|
100
|
+
lines.push('## Next-Pass Direction');
|
|
101
|
+
lines.push('');
|
|
102
|
+
|
|
103
|
+
// Aggregate preserve/correct across all candidates
|
|
104
|
+
const allPreserve = new Set();
|
|
105
|
+
const allCorrect = new Set();
|
|
106
|
+
for (const c of report.candidates) {
|
|
107
|
+
for (const p of c.preserve_next_pass) allPreserve.add(p);
|
|
108
|
+
for (const x of c.correct_next_pass) allCorrect.add(x);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (allPreserve.size > 0) {
|
|
112
|
+
lines.push('**Preserve:**');
|
|
113
|
+
for (const p of allPreserve) lines.push(`- ${p}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (allCorrect.size > 0) {
|
|
118
|
+
lines.push('**Correct:**');
|
|
119
|
+
for (const x of allCorrect) lines.push(`- ${x}`);
|
|
120
|
+
lines.push('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Terminal text renderer ──────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Render a critique report as plain text for terminal display.
|
|
130
|
+
*/
|
|
131
|
+
export function renderCritiqueText(report) {
|
|
132
|
+
const lines = [];
|
|
133
|
+
|
|
134
|
+
lines.push(`Critique: ${report.run_id}`);
|
|
135
|
+
lines.push(`Brief: ${report.brief_id} Workflow: ${report.workflow_id}`);
|
|
136
|
+
if (report.subject_id) lines.push(`Subject: ${report.subject_id}`);
|
|
137
|
+
lines.push('');
|
|
138
|
+
|
|
139
|
+
// Action
|
|
140
|
+
const action = report.recommended_action;
|
|
141
|
+
const actionIcon = {
|
|
142
|
+
accept_one: '✓',
|
|
143
|
+
refine_from_one: '→',
|
|
144
|
+
rerun_broader: '↻',
|
|
145
|
+
discard_run: '✗',
|
|
146
|
+
};
|
|
147
|
+
lines.push(`${actionIcon[action.mode] || '?'} ${action.mode.replace(/_/g, ' ')}`);
|
|
148
|
+
if (action.preferred_candidate) lines.push(` Preferred: ${action.preferred_candidate}`);
|
|
149
|
+
if (action.reason) lines.push(` ${action.reason}`);
|
|
150
|
+
lines.push('');
|
|
151
|
+
|
|
152
|
+
// Dimensions
|
|
153
|
+
lines.push(`Dimensions: ${report.active_dimensions.map(d => d.replace(/_/g, ' ')).join(', ')}`);
|
|
154
|
+
lines.push('');
|
|
155
|
+
|
|
156
|
+
// Candidates
|
|
157
|
+
for (const c of report.candidates) {
|
|
158
|
+
const badge = FIT_BADGES[c.overall_fit] || '? unknown';
|
|
159
|
+
lines.push(` ${badge} ${c.filename} (seed: ${c.seed ?? '?'})`);
|
|
160
|
+
|
|
161
|
+
if (c.strengths.length > 0) {
|
|
162
|
+
lines.push(` strengths: ${c.strengths.slice(0, 3).join('; ')}`);
|
|
163
|
+
}
|
|
164
|
+
if (c.drift_issues.length > 0) {
|
|
165
|
+
lines.push(` drift: ${c.drift_issues.slice(0, 3).join('; ')}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
lines.push('');
|
|
169
|
+
|
|
170
|
+
return lines.join('\n');
|
|
171
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error shape for Style Dataset Lab.
|
|
3
|
+
*
|
|
4
|
+
* Every user-facing error has: code, message, hint.
|
|
5
|
+
* Exit codes: 0 ok, 1 user error, 2 runtime error.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class SdlabError extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} code — error code (e.g. INPUT_MISSING_FLAG)
|
|
11
|
+
* @param {string} message — human-readable description
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {string} [options.hint] — actionable fix suggestion
|
|
14
|
+
* @param {Error} [options.cause] — original error
|
|
15
|
+
* @param {number} [options.exitCode] — 1 (user) or 2 (runtime)
|
|
16
|
+
*/
|
|
17
|
+
constructor(code, message, { hint, cause, exitCode } = {}) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'SdlabError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.hint = hint || null;
|
|
22
|
+
this.cause = cause || null;
|
|
23
|
+
this.exitCode = exitCode || 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toJSON() {
|
|
27
|
+
return {
|
|
28
|
+
code: this.code,
|
|
29
|
+
message: this.message,
|
|
30
|
+
hint: this.hint,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Convenience constructors ──────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export function inputError(code, message, hint) {
|
|
38
|
+
return new SdlabError(code, message, { hint, exitCode: 1 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function runtimeError(code, message, hint, cause) {
|
|
42
|
+
return new SdlabError(code, message, { hint, cause, exitCode: 2 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── CLI error handler ─────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
let debugMode = false;
|
|
48
|
+
|
|
49
|
+
export function enableDebug() {
|
|
50
|
+
debugMode = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isDebug() {
|
|
54
|
+
return debugMode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle an error at the CLI boundary.
|
|
59
|
+
* Shows structured output by default, stack trace with --debug.
|
|
60
|
+
*/
|
|
61
|
+
export function handleCliError(err) {
|
|
62
|
+
if (err instanceof SdlabError) {
|
|
63
|
+
console.error(`\x1b[31mError [${err.code}]:\x1b[0m ${err.message}`);
|
|
64
|
+
if (err.hint) {
|
|
65
|
+
console.error(`\x1b[33mHint:\x1b[0m ${err.hint}`);
|
|
66
|
+
}
|
|
67
|
+
if (debugMode && err.stack) {
|
|
68
|
+
console.error('\n' + err.stack);
|
|
69
|
+
}
|
|
70
|
+
if (debugMode && err.cause) {
|
|
71
|
+
console.error('\nCaused by:', err.cause);
|
|
72
|
+
}
|
|
73
|
+
process.exit(err.exitCode);
|
|
74
|
+
} else {
|
|
75
|
+
// Unstructured error — runtime failure
|
|
76
|
+
console.error(`\x1b[31mError:\x1b[0m ${err.message || err}`);
|
|
77
|
+
if (debugMode && err.stack) {
|
|
78
|
+
console.error('\n' + err.stack);
|
|
79
|
+
}
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated provenance builder — constructs the provenance block
|
|
3
|
+
* attached to records created from selected run/batch outputs.
|
|
4
|
+
*
|
|
5
|
+
* Normalizes source context (run vs batch) into a single shape
|
|
6
|
+
* that traces back through brief → workflow → seed → config.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a generation provenance payload for a selected output.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} opts
|
|
17
|
+
* @param {'run'|'batch'} opts.sourceType
|
|
18
|
+
* @param {string} opts.sourceId — run_id or batch_id
|
|
19
|
+
* @param {Object} opts.runManifest — the run manifest containing this output
|
|
20
|
+
* @param {Object} [opts.batchManifest] — batch manifest if source is batch
|
|
21
|
+
* @param {Object} opts.item — the selection item (slot_or_output, filename, seed)
|
|
22
|
+
* @param {string} opts.projectRoot
|
|
23
|
+
* @returns {Object} generation_provenance block
|
|
24
|
+
*/
|
|
25
|
+
export function buildGeneratedProvenance({
|
|
26
|
+
sourceType,
|
|
27
|
+
sourceId,
|
|
28
|
+
runManifest,
|
|
29
|
+
batchManifest,
|
|
30
|
+
item,
|
|
31
|
+
projectRoot,
|
|
32
|
+
}) {
|
|
33
|
+
const prov = {
|
|
34
|
+
source_type: sourceType,
|
|
35
|
+
source_id: sourceId,
|
|
36
|
+
run_id: runManifest.run_id,
|
|
37
|
+
brief_id: runManifest.brief_id,
|
|
38
|
+
workflow_id: runManifest.workflow_template_id,
|
|
39
|
+
selected_filename: item.filename,
|
|
40
|
+
generated_at: runManifest.created_at,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (sourceType === 'batch' && batchManifest) {
|
|
44
|
+
prov.batch_id = batchManifest.batch_id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Seed from the item or from the run manifest outputs
|
|
48
|
+
if (item.seed !== undefined) {
|
|
49
|
+
prov.seed = item.seed;
|
|
50
|
+
} else if (runManifest.outputs) {
|
|
51
|
+
const match = runManifest.outputs.find(o => o.filename === item.filename);
|
|
52
|
+
if (match?.seed !== undefined) prov.seed = match.seed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Adapter target from run manifest
|
|
56
|
+
if (runManifest.adapter_target) {
|
|
57
|
+
prov.adapter_target = runManifest.adapter_target;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Subject from run brief or batch manifest
|
|
61
|
+
if (batchManifest?.subject_id) {
|
|
62
|
+
prov.subject_id = batchManifest.subject_id;
|
|
63
|
+
} else {
|
|
64
|
+
// Try to read subject from brief
|
|
65
|
+
const briefPath = join(projectRoot, 'briefs', `${runManifest.brief_id}.json`);
|
|
66
|
+
if (existsSync(briefPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const brief = JSON.parse(readFileSync(briefPath, 'utf-8'));
|
|
69
|
+
if (brief.subject_id) prov.subject_id = brief.subject_id;
|
|
70
|
+
if (brief.parent_brief_id) prov.parent_brief_id = brief.parent_brief_id;
|
|
71
|
+
} catch { /* skip */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Config fingerprint — hash the run manifest for reproducibility tracing
|
|
76
|
+
prov.config_fingerprint = createHash('sha256')
|
|
77
|
+
.update(JSON.stringify({
|
|
78
|
+
brief_id: runManifest.brief_id,
|
|
79
|
+
workflow_template_id: runManifest.workflow_template_id,
|
|
80
|
+
adapter_target: runManifest.adapter_target,
|
|
81
|
+
seed_plan: runManifest.seed_plan,
|
|
82
|
+
}))
|
|
83
|
+
.digest('hex')
|
|
84
|
+
.slice(0, 16);
|
|
85
|
+
|
|
86
|
+
return prov;
|
|
87
|
+
}
|
package/lib/log.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging levels for CLI output.
|
|
3
|
+
*
|
|
4
|
+
* Levels: quiet < normal < verbose
|
|
5
|
+
* Set via --quiet or --verbose flags (parsed by caller).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
let level = 'normal'; // quiet | normal | verbose
|
|
9
|
+
|
|
10
|
+
export function setLogLevel(l) {
|
|
11
|
+
level = l;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getLogLevel() {
|
|
15
|
+
return level;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Always shown (errors, final results). */
|
|
19
|
+
export function log(...args) {
|
|
20
|
+
console.log(...args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Shown at normal and verbose levels (progress, summaries). */
|
|
24
|
+
export function info(...args) {
|
|
25
|
+
if (level !== 'quiet') console.log(...args);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Shown only at verbose level (per-record details, debug context). */
|
|
29
|
+
export function verbose(...args) {
|
|
30
|
+
if (level === 'verbose') console.log(...args);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Always shown on stderr. */
|
|
34
|
+
export function warn(...args) {
|
|
35
|
+
console.error('\x1b[33m⚠\x1b[0m', ...args);
|
|
36
|
+
}
|
package/lib/paths.js
CHANGED
|
@@ -36,3 +36,34 @@ export function getProjectRoot(name) {
|
|
|
36
36
|
export function resolveProjectPath(name, ...segments) {
|
|
37
37
|
return join(getProjectRoot(name), ...segments);
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Return the workflow profiles directory for a project root.
|
|
42
|
+
* @param {string} projectRoot — absolute path to project
|
|
43
|
+
*/
|
|
44
|
+
export function getWorkflowProfilesDir(projectRoot) {
|
|
45
|
+
return join(projectRoot, 'workflows', 'profiles');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Return the briefs directory for a project root.
|
|
50
|
+
* @param {string} projectRoot — absolute path to project
|
|
51
|
+
*/
|
|
52
|
+
export function getBriefsDir(projectRoot) {
|
|
53
|
+
return join(projectRoot, 'briefs');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Return the runs directory for a project root.
|
|
58
|
+
* @param {string} projectRoot — absolute path to project
|
|
59
|
+
*/
|
|
60
|
+
export function getRunsDir(projectRoot) {
|
|
61
|
+
return join(projectRoot, 'runs');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Return the runtime templates directory (repo-level).
|
|
66
|
+
*/
|
|
67
|
+
export function getRuntimeDir() {
|
|
68
|
+
return join(REPO_ROOT, 'runtime');
|
|
69
|
+
}
|