@qlucent/fishi-core 0.6.0 → 0.7.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/index.d.ts +57 -1
- package/dist/index.js +208 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -568,6 +568,62 @@ declare function getAgentSummary(projectDir: string): Record<string, {
|
|
|
568
568
|
filesChanged: number;
|
|
569
569
|
}>;
|
|
570
570
|
|
|
571
|
+
type SandboxMode = 'docker' | 'process';
|
|
572
|
+
interface SandboxConfig {
|
|
573
|
+
mode: SandboxMode;
|
|
574
|
+
dockerAvailable: boolean;
|
|
575
|
+
}
|
|
576
|
+
interface SandboxPolicy {
|
|
577
|
+
networkAllow: string[];
|
|
578
|
+
envPassthrough: string[];
|
|
579
|
+
timeout: number;
|
|
580
|
+
memory: string;
|
|
581
|
+
cpus: number;
|
|
582
|
+
}
|
|
583
|
+
interface SandboxRunResult {
|
|
584
|
+
stdout: string;
|
|
585
|
+
stderr: string;
|
|
586
|
+
exitCode: number;
|
|
587
|
+
timedOut: boolean;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Detect if Docker is installed and running.
|
|
591
|
+
*/
|
|
592
|
+
declare function detectDocker(): boolean;
|
|
593
|
+
/**
|
|
594
|
+
* Read sandbox config from .fishi/fishi.yaml
|
|
595
|
+
*/
|
|
596
|
+
declare function readSandboxConfig(projectDir: string): SandboxConfig;
|
|
597
|
+
/**
|
|
598
|
+
* Read sandbox policy from .fishi/sandbox-policy.yaml, or return defaults.
|
|
599
|
+
*/
|
|
600
|
+
declare function readSandboxPolicy(projectDir: string): SandboxPolicy;
|
|
601
|
+
/**
|
|
602
|
+
* Build a restricted env object for process mode.
|
|
603
|
+
* Strips all env vars except explicitly allowed ones + essentials.
|
|
604
|
+
*/
|
|
605
|
+
declare function buildSandboxEnv(policy: SandboxPolicy): Record<string, string>;
|
|
606
|
+
/**
|
|
607
|
+
* Run a command in process-mode sandbox.
|
|
608
|
+
*/
|
|
609
|
+
declare function runInProcessSandbox(command: string, args: string[], worktreePath: string, policy: SandboxPolicy): SandboxRunResult;
|
|
610
|
+
/**
|
|
611
|
+
* Run a command in Docker sandbox.
|
|
612
|
+
*/
|
|
613
|
+
declare function runInDockerSandbox(command: string, args: string[], worktreePath: string, policy: SandboxPolicy, options?: {
|
|
614
|
+
nodeModulesPath?: string;
|
|
615
|
+
}): SandboxRunResult;
|
|
616
|
+
/**
|
|
617
|
+
* Run a command in the configured sandbox mode.
|
|
618
|
+
*/
|
|
619
|
+
declare function runInSandbox(command: string, args: string[], worktreePath: string, projectDir: string, options?: {
|
|
620
|
+
nodeModulesPath?: string;
|
|
621
|
+
}): SandboxRunResult;
|
|
622
|
+
|
|
623
|
+
declare function getSandboxPolicyTemplate(): string;
|
|
624
|
+
|
|
625
|
+
declare function getDockerfileTemplate(): string;
|
|
626
|
+
|
|
571
627
|
declare function getDashboardHtml(): string;
|
|
572
628
|
|
|
573
|
-
export { type AgentDefinition, type AgentRole, type AgentTemplate, type BackupManifest, type BrownfieldAnalysisData, type ClaudeMdOptions, type CommandTemplate, type ConflictCategory, type ConflictMap, type ConflictResolution, type CostMode, type DetectionCheck, type DetectionResult, type DynamicAgent, type DynamicAgentConfig, type ExecutionConfig, type FileConflict, type FileResolutionMap, type FishiConfig, type FishiYamlOptions, type GateConfig, type GateStatus, type GitConfig, type HookTemplate, type InitOptions, type McpConfig, type McpServerConfig, type ModelRoutingConfig, type ModelTier, type MonitorEvent, type MonitorState, type MonitorSummary, type ProjectConfig, type ProjectType, type ProjectYamlOptions, type ScaffoldOptions, type ScaffoldResult, type SkillTemplate, type StateConfig, type TaskStatus, type TaskboardConfig, type TemplateContext, architectAgentTemplate, backendAgentTemplate, createBackup, detectConflicts, devLeadTemplate, devopsAgentTemplate, docsAgentTemplate, emitEvent, frontendAgentTemplate, fullstackAgentTemplate, generateScaffold, getAdaptiveTaskGraphSkill, getAgentCompleteHook, getAgentFactoryTemplate, getAgentRegistryTemplate, getAgentSummary, getApiDesignSkill, getAutoCheckpointHook, getBoardCommand, getBrainstormingSkill, getBrownfieldAnalysisSkill, getBrownfieldDiscoverySkill, getClaudeMdTemplate, getCodeGenSkill, getCoordinatorFactoryTemplate, getDashboardHtml, getDebuggingSkill, getDeploymentSkill, getDocCheckerScript, getDocumentationSkill, getFishiYamlTemplate, getGateCommand, getGateManagerScript, getGitignoreAdditions, getInitCommand, getLearningsManagerScript, getMasterOrchestratorTemplate, getMcpJsonTemplate, getMemoryManagerScript, getModelRoutingReference, getMonitorEmitterScript, getPhaseRunnerScript, getPostEditHook, getPrdCommand, getPrdSkill, getProjectYamlTemplate, getResetCommand, getResumeCommand, getSafetyCheckHook, getSessionStartHook, getSettingsJsonTemplate, getSprintCommand, getStatusCommand, getTaskboardOpsSkill, getTaskboardUpdateHook, getTestingSkill, getTodoManagerScript, getValidateScaffoldScript, getWorktreeManagerScript, getWorktreeSetupHook, marketingAgentTemplate, mergeClaudeMd, mergeClaudeMdTop, mergeGitignore, mergeMcpJson, mergeSettingsJson, opsLeadTemplate, planningAgentTemplate, planningLeadTemplate, qualityLeadTemplate, readMonitorState, researchAgentTemplate, securityAgentTemplate, testingAgentTemplate, uiuxAgentTemplate, writingAgentTemplate };
|
|
629
|
+
export { type AgentDefinition, type AgentRole, type AgentTemplate, type BackupManifest, type BrownfieldAnalysisData, type ClaudeMdOptions, type CommandTemplate, type ConflictCategory, type ConflictMap, type ConflictResolution, type CostMode, type DetectionCheck, type DetectionResult, type DynamicAgent, type DynamicAgentConfig, type ExecutionConfig, type FileConflict, type FileResolutionMap, type FishiConfig, type FishiYamlOptions, type GateConfig, type GateStatus, type GitConfig, type HookTemplate, type InitOptions, type McpConfig, type McpServerConfig, type ModelRoutingConfig, type ModelTier, type MonitorEvent, type MonitorState, type MonitorSummary, type ProjectConfig, type ProjectType, type ProjectYamlOptions, type SandboxConfig, type SandboxMode, type SandboxPolicy, type SandboxRunResult, type ScaffoldOptions, type ScaffoldResult, type SkillTemplate, type StateConfig, type TaskStatus, type TaskboardConfig, type TemplateContext, architectAgentTemplate, backendAgentTemplate, buildSandboxEnv, createBackup, detectConflicts, detectDocker, devLeadTemplate, devopsAgentTemplate, docsAgentTemplate, emitEvent, frontendAgentTemplate, fullstackAgentTemplate, generateScaffold, getAdaptiveTaskGraphSkill, getAgentCompleteHook, getAgentFactoryTemplate, getAgentRegistryTemplate, getAgentSummary, getApiDesignSkill, getAutoCheckpointHook, getBoardCommand, getBrainstormingSkill, getBrownfieldAnalysisSkill, getBrownfieldDiscoverySkill, getClaudeMdTemplate, getCodeGenSkill, getCoordinatorFactoryTemplate, getDashboardHtml, getDebuggingSkill, getDeploymentSkill, getDocCheckerScript, getDockerfileTemplate, getDocumentationSkill, getFishiYamlTemplate, getGateCommand, getGateManagerScript, getGitignoreAdditions, getInitCommand, getLearningsManagerScript, getMasterOrchestratorTemplate, getMcpJsonTemplate, getMemoryManagerScript, getModelRoutingReference, getMonitorEmitterScript, getPhaseRunnerScript, getPostEditHook, getPrdCommand, getPrdSkill, getProjectYamlTemplate, getResetCommand, getResumeCommand, getSafetyCheckHook, getSandboxPolicyTemplate, getSessionStartHook, getSettingsJsonTemplate, getSprintCommand, getStatusCommand, getTaskboardOpsSkill, getTaskboardUpdateHook, getTestingSkill, getTodoManagerScript, getValidateScaffoldScript, getWorktreeManagerScript, getWorktreeSetupHook, marketingAgentTemplate, mergeClaudeMd, mergeClaudeMdTop, mergeGitignore, mergeMcpJson, mergeSettingsJson, opsLeadTemplate, planningAgentTemplate, planningLeadTemplate, qualityLeadTemplate, readMonitorState, readSandboxConfig, readSandboxPolicy, researchAgentTemplate, runInDockerSandbox, runInProcessSandbox, runInSandbox, securityAgentTemplate, testingAgentTemplate, uiuxAgentTemplate, writingAgentTemplate };
|
package/dist/index.js
CHANGED
|
@@ -11111,7 +11111,7 @@ async function createBackup(targetDir, conflictingFiles) {
|
|
|
11111
11111
|
manifestFiles.push({ path: relPath, size: stat.size });
|
|
11112
11112
|
}
|
|
11113
11113
|
}
|
|
11114
|
-
const fishiVersion = "0.
|
|
11114
|
+
const fishiVersion = "0.7.0";
|
|
11115
11115
|
const manifest = {
|
|
11116
11116
|
timestamp: now.toISOString(),
|
|
11117
11117
|
fishi_version: fishiVersion,
|
|
@@ -11218,6 +11218,204 @@ function getAgentSummary(projectDir) {
|
|
|
11218
11218
|
return agents;
|
|
11219
11219
|
}
|
|
11220
11220
|
|
|
11221
|
+
// src/generators/sandbox.ts
|
|
11222
|
+
import { execSync, execFileSync } from "child_process";
|
|
11223
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
11224
|
+
import { join as join5 } from "path";
|
|
11225
|
+
var DEFAULT_POLICY = {
|
|
11226
|
+
networkAllow: ["registry.npmjs.org", "localhost", "127.0.0.1"],
|
|
11227
|
+
envPassthrough: [],
|
|
11228
|
+
timeout: 600,
|
|
11229
|
+
// 10 minutes
|
|
11230
|
+
memory: "2g",
|
|
11231
|
+
cpus: 2
|
|
11232
|
+
};
|
|
11233
|
+
function detectDocker() {
|
|
11234
|
+
try {
|
|
11235
|
+
execSync("docker info", { stdio: "pipe", timeout: 5e3 });
|
|
11236
|
+
return true;
|
|
11237
|
+
} catch {
|
|
11238
|
+
return false;
|
|
11239
|
+
}
|
|
11240
|
+
}
|
|
11241
|
+
function readSandboxConfig(projectDir) {
|
|
11242
|
+
const yamlPath = join5(projectDir, ".fishi", "fishi.yaml");
|
|
11243
|
+
if (!existsSync5(yamlPath)) {
|
|
11244
|
+
return { mode: "process", dockerAvailable: false };
|
|
11245
|
+
}
|
|
11246
|
+
const content = readFileSync2(yamlPath, "utf-8");
|
|
11247
|
+
const modeMatch = content.match(/^\s*mode:\s*(docker|process)/m);
|
|
11248
|
+
const dockerMatch = content.match(/^\s*docker_available:\s*(true|false)/m);
|
|
11249
|
+
return {
|
|
11250
|
+
mode: modeMatch?.[1] || "process",
|
|
11251
|
+
dockerAvailable: dockerMatch?.[1] === "true"
|
|
11252
|
+
};
|
|
11253
|
+
}
|
|
11254
|
+
function readSandboxPolicy(projectDir) {
|
|
11255
|
+
const policyPath = join5(projectDir, ".fishi", "sandbox-policy.yaml");
|
|
11256
|
+
if (!existsSync5(policyPath)) return { ...DEFAULT_POLICY };
|
|
11257
|
+
const content = readFileSync2(policyPath, "utf-8");
|
|
11258
|
+
const networkAllow = extractYamlList(content, "network_allow") || DEFAULT_POLICY.networkAllow;
|
|
11259
|
+
const envPassthrough = extractYamlList(content, "env_passthrough") || DEFAULT_POLICY.envPassthrough;
|
|
11260
|
+
const timeoutMatch = content.match(/^\s*timeout:\s*(\d+)/m);
|
|
11261
|
+
const memoryMatch = content.match(/^\s*memory:\s*["']?(\S+?)["']?\s*$/m);
|
|
11262
|
+
const cpusMatch = content.match(/^\s*cpus:\s*(\d+)/m);
|
|
11263
|
+
return {
|
|
11264
|
+
networkAllow,
|
|
11265
|
+
envPassthrough,
|
|
11266
|
+
timeout: timeoutMatch ? parseInt(timeoutMatch[1], 10) : DEFAULT_POLICY.timeout,
|
|
11267
|
+
memory: memoryMatch?.[1] || DEFAULT_POLICY.memory,
|
|
11268
|
+
cpus: cpusMatch ? parseInt(cpusMatch[1], 10) : DEFAULT_POLICY.cpus
|
|
11269
|
+
};
|
|
11270
|
+
}
|
|
11271
|
+
function extractYamlList(content, key) {
|
|
11272
|
+
const regex = new RegExp(`^\\s*${key}:\\s*\\n((?:\\s+-\\s*.+\\n?)*)`, "m");
|
|
11273
|
+
const match = content.match(regex);
|
|
11274
|
+
if (!match) return null;
|
|
11275
|
+
return match[1].split("\n").map((line) => {
|
|
11276
|
+
const m = line.match(/^\s*-\s*["']?(.+?)["']?\s*$/);
|
|
11277
|
+
return m ? m[1] : "";
|
|
11278
|
+
}).filter(Boolean);
|
|
11279
|
+
}
|
|
11280
|
+
function buildSandboxEnv(policy) {
|
|
11281
|
+
const env = {
|
|
11282
|
+
PATH: process.env.PATH || "",
|
|
11283
|
+
HOME: process.env.HOME || process.env.USERPROFILE || "",
|
|
11284
|
+
NODE_ENV: "development",
|
|
11285
|
+
FISHI_SANDBOX: "true"
|
|
11286
|
+
};
|
|
11287
|
+
for (const key of policy.envPassthrough) {
|
|
11288
|
+
if (process.env[key]) {
|
|
11289
|
+
env[key] = process.env[key];
|
|
11290
|
+
}
|
|
11291
|
+
}
|
|
11292
|
+
return env;
|
|
11293
|
+
}
|
|
11294
|
+
function runInProcessSandbox(command, args, worktreePath, policy) {
|
|
11295
|
+
const env = buildSandboxEnv(policy);
|
|
11296
|
+
try {
|
|
11297
|
+
const stdout = execFileSync(command, args, {
|
|
11298
|
+
cwd: worktreePath,
|
|
11299
|
+
encoding: "utf-8",
|
|
11300
|
+
timeout: policy.timeout * 1e3,
|
|
11301
|
+
env,
|
|
11302
|
+
stdio: "pipe",
|
|
11303
|
+
maxBuffer: 10 * 1024 * 1024
|
|
11304
|
+
// 10MB
|
|
11305
|
+
});
|
|
11306
|
+
return { stdout, stderr: "", exitCode: 0, timedOut: false };
|
|
11307
|
+
} catch (e) {
|
|
11308
|
+
if (e.killed || e.signal === "SIGTERM") {
|
|
11309
|
+
return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: 1, timedOut: true };
|
|
11310
|
+
}
|
|
11311
|
+
return {
|
|
11312
|
+
stdout: e.stdout || "",
|
|
11313
|
+
stderr: e.stderr || "",
|
|
11314
|
+
exitCode: e.status ?? 1,
|
|
11315
|
+
timedOut: false
|
|
11316
|
+
};
|
|
11317
|
+
}
|
|
11318
|
+
}
|
|
11319
|
+
function runInDockerSandbox(command, args, worktreePath, policy, options = {}) {
|
|
11320
|
+
const dockerArgs = [
|
|
11321
|
+
"run",
|
|
11322
|
+
"--rm",
|
|
11323
|
+
"--workdir",
|
|
11324
|
+
"/workspace",
|
|
11325
|
+
"-v",
|
|
11326
|
+
`${worktreePath}:/workspace`
|
|
11327
|
+
];
|
|
11328
|
+
if (options.nodeModulesPath && existsSync5(options.nodeModulesPath)) {
|
|
11329
|
+
dockerArgs.push("-v", `${options.nodeModulesPath}:/workspace/node_modules:ro`);
|
|
11330
|
+
}
|
|
11331
|
+
dockerArgs.push("--memory", policy.memory);
|
|
11332
|
+
dockerArgs.push("--cpus", String(policy.cpus));
|
|
11333
|
+
dockerArgs.push("-e", "FISHI_SANDBOX=true");
|
|
11334
|
+
dockerArgs.push("-e", "NODE_ENV=development");
|
|
11335
|
+
for (const key of policy.envPassthrough) {
|
|
11336
|
+
if (process.env[key]) {
|
|
11337
|
+
dockerArgs.push("-e", `${key}=${process.env[key]}`);
|
|
11338
|
+
}
|
|
11339
|
+
}
|
|
11340
|
+
dockerArgs.push("--network", "host");
|
|
11341
|
+
dockerArgs.push("fishi-sandbox:latest", command, ...args);
|
|
11342
|
+
try {
|
|
11343
|
+
const stdout = execFileSync("docker", dockerArgs, {
|
|
11344
|
+
encoding: "utf-8",
|
|
11345
|
+
timeout: policy.timeout * 1e3,
|
|
11346
|
+
stdio: "pipe",
|
|
11347
|
+
maxBuffer: 10 * 1024 * 1024
|
|
11348
|
+
});
|
|
11349
|
+
return { stdout, stderr: "", exitCode: 0, timedOut: false };
|
|
11350
|
+
} catch (e) {
|
|
11351
|
+
if (e.killed || e.signal === "SIGTERM") {
|
|
11352
|
+
return { stdout: e.stdout || "", stderr: e.stderr || "", exitCode: 1, timedOut: true };
|
|
11353
|
+
}
|
|
11354
|
+
return {
|
|
11355
|
+
stdout: e.stdout || "",
|
|
11356
|
+
stderr: e.stderr || "",
|
|
11357
|
+
exitCode: e.status ?? 1,
|
|
11358
|
+
timedOut: false
|
|
11359
|
+
};
|
|
11360
|
+
}
|
|
11361
|
+
}
|
|
11362
|
+
function runInSandbox(command, args, worktreePath, projectDir, options = {}) {
|
|
11363
|
+
const config = readSandboxConfig(projectDir);
|
|
11364
|
+
const policy = readSandboxPolicy(projectDir);
|
|
11365
|
+
if (config.mode === "docker" && config.dockerAvailable) {
|
|
11366
|
+
return runInDockerSandbox(command, args, worktreePath, policy, options);
|
|
11367
|
+
}
|
|
11368
|
+
return runInProcessSandbox(command, args, worktreePath, policy);
|
|
11369
|
+
}
|
|
11370
|
+
|
|
11371
|
+
// src/templates/configs/sandbox-policy.ts
|
|
11372
|
+
function getSandboxPolicyTemplate() {
|
|
11373
|
+
return `# FISHI Sandbox Policy
|
|
11374
|
+
# Controls what agents can access inside their sandboxed worktrees
|
|
11375
|
+
|
|
11376
|
+
# Network domains agents are allowed to reach
|
|
11377
|
+
network_allow:
|
|
11378
|
+
- registry.npmjs.org
|
|
11379
|
+
- localhost
|
|
11380
|
+
- 127.0.0.1
|
|
11381
|
+
|
|
11382
|
+
# Environment variables passed into the sandbox
|
|
11383
|
+
# Add secrets your agents need (e.g., DATABASE_URL, API_KEY)
|
|
11384
|
+
env_passthrough: []
|
|
11385
|
+
|
|
11386
|
+
# Maximum time (seconds) a single agent command can run
|
|
11387
|
+
timeout: 600
|
|
11388
|
+
|
|
11389
|
+
# Docker resource limits (only applies in docker mode)
|
|
11390
|
+
memory: "2g"
|
|
11391
|
+
cpus: 2
|
|
11392
|
+
`;
|
|
11393
|
+
}
|
|
11394
|
+
|
|
11395
|
+
// src/templates/docker/Dockerfile.ts
|
|
11396
|
+
function getDockerfileTemplate() {
|
|
11397
|
+
return `# FISHI Sandbox Runtime
|
|
11398
|
+
# Minimal Node.js image for agent execution
|
|
11399
|
+
FROM node:22-slim
|
|
11400
|
+
|
|
11401
|
+
# Install git and common build tools
|
|
11402
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
11403
|
+
git \\
|
|
11404
|
+
ca-certificates \\
|
|
11405
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11406
|
+
|
|
11407
|
+
# Set working directory
|
|
11408
|
+
WORKDIR /workspace
|
|
11409
|
+
|
|
11410
|
+
# Non-root user for additional safety
|
|
11411
|
+
RUN groupadd -r fishi && useradd -r -g fishi -m fishi
|
|
11412
|
+
USER fishi
|
|
11413
|
+
|
|
11414
|
+
# Default command
|
|
11415
|
+
CMD ["node"]
|
|
11416
|
+
`;
|
|
11417
|
+
}
|
|
11418
|
+
|
|
11221
11419
|
// src/templates/dashboard/index-html.ts
|
|
11222
11420
|
function getDashboardHtml() {
|
|
11223
11421
|
return `<!DOCTYPE html>
|
|
@@ -11564,8 +11762,10 @@ function getDashboardHtml() {
|
|
|
11564
11762
|
export {
|
|
11565
11763
|
architectAgentTemplate,
|
|
11566
11764
|
backendAgentTemplate,
|
|
11765
|
+
buildSandboxEnv,
|
|
11567
11766
|
createBackup,
|
|
11568
11767
|
detectConflicts,
|
|
11768
|
+
detectDocker,
|
|
11569
11769
|
devLeadTemplate,
|
|
11570
11770
|
devopsAgentTemplate,
|
|
11571
11771
|
docsAgentTemplate,
|
|
@@ -11591,6 +11791,7 @@ export {
|
|
|
11591
11791
|
getDebuggingSkill,
|
|
11592
11792
|
getDeploymentSkill,
|
|
11593
11793
|
getDocCheckerScript,
|
|
11794
|
+
getDockerfileTemplate,
|
|
11594
11795
|
getDocumentationSkill,
|
|
11595
11796
|
getFishiYamlTemplate,
|
|
11596
11797
|
getGateCommand,
|
|
@@ -11611,6 +11812,7 @@ export {
|
|
|
11611
11812
|
getResetCommand,
|
|
11612
11813
|
getResumeCommand,
|
|
11613
11814
|
getSafetyCheckHook,
|
|
11815
|
+
getSandboxPolicyTemplate,
|
|
11614
11816
|
getSessionStartHook,
|
|
11615
11817
|
getSettingsJsonTemplate,
|
|
11616
11818
|
getSprintCommand,
|
|
@@ -11633,7 +11835,12 @@ export {
|
|
|
11633
11835
|
planningLeadTemplate,
|
|
11634
11836
|
qualityLeadTemplate,
|
|
11635
11837
|
readMonitorState,
|
|
11838
|
+
readSandboxConfig,
|
|
11839
|
+
readSandboxPolicy,
|
|
11636
11840
|
researchAgentTemplate,
|
|
11841
|
+
runInDockerSandbox,
|
|
11842
|
+
runInProcessSandbox,
|
|
11843
|
+
runInSandbox,
|
|
11637
11844
|
securityAgentTemplate,
|
|
11638
11845
|
testingAgentTemplate,
|
|
11639
11846
|
uiuxAgentTemplate,
|