@tdk-landscape/tdk-cli-core 1.3.107 → 1.3.109

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@tdk-landscape/tdk-cli-core.svg?style=flat&color=blue)](https://www.npmjs.com/package/@tdk-landscape/tdk-cli-core)
4
4
  [![unpacked size](https://img.shields.io/npm/unpacked-size/@tdk-landscape/tdk-cli-core)](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
5
- [![install size](https://packagephobia.com/badge?p=@tdk-landscape/tdk-cli-core)](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
5
+ [![install size](https://badgen.net/packagephobia/install/@tdk-landscape/tdk-cli-core)](https://packagephobia.com/result?p=@tdk-landscape/tdk-cli-core)
6
6
  [![CI](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/ci.yml)
7
7
  [![Quickstart E2E](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml)
8
8
  [![ERP fixture scale E2E](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml/badge.svg)](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml)
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { renderToString } from "ink";
3
+ import { describe, expect, it } from "vitest";
4
+ import { LoadingScreen } from "../ui.js";
5
+ describe("ui LoadingScreen", () => {
6
+ it("shows honest text with no percentage", () => {
7
+ const out = renderToString(_jsx(LoadingScreen, { animated: false }));
8
+ expect(out).toContain("Discovering resources...");
9
+ expect(out).not.toContain("%");
10
+ });
11
+ it("renders a spinner frame only when animated", () => {
12
+ expect(renderToString(_jsx(LoadingScreen, { animated: true }))).toMatch(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Discovering/);
13
+ expect(renderToString(_jsx(LoadingScreen, { animated: false }))).not.toMatch(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/);
14
+ });
15
+ });
16
+ //# sourceMappingURL=ui-loading-screen.test.js.map
@@ -4,7 +4,7 @@ import { Command } from "commander";
4
4
  import { readProjectConfig } from "../generator/template-engine.js";
5
5
  import { getStackEmoji } from "../utils/constants.js";
6
6
  import { createDiscoveryContext } from "../utils/discovery-context.js";
7
- import { logVerbose, requireProjectRoot } from "../utils/errors.js";
7
+ import { errorFactories, logVerbose, requireProjectRoot } from "../utils/errors.js";
8
8
  import { colorizeByStatus, DEFAULT_BOX_WIDTH, formatBoxLine, formatPadded, getStatusIcon, printBoxedHeader, } from "../utils/formatting.js";
9
9
  import { readSavedHostPortPlan } from "../utils/host-port-config.js";
10
10
  import { createHostPortPlan } from "../utils/host-port-plan.js";
@@ -12,6 +12,7 @@ import { createMachineEnvelope, writeMachineError } from "../utils/machine-outpu
12
12
  import { findProjectRoot } from "../utils/paths.js";
13
13
  import { checkPortStatus } from "../utils/port-assignment.js";
14
14
  import { isApiServiceType } from "../utils/resource-kind.js";
15
+ import { stackExists } from "../utils/services.js";
15
16
  import { isValidPort, sanitizeForShell } from "../utils/validation.js";
16
17
  import { findOnPath } from "../utils/which.js";
17
18
  function execSafe(command, args, options = {}) {
@@ -169,10 +170,16 @@ export const networksCommand = new Command("networks")
169
170
  if (!projectRoot) {
170
171
  throw new Error("Could not find project root (no .tdk/project.json found)");
171
172
  }
173
+ const discovery = createDiscoveryContext();
174
+ if (options.stack && !stackExists(options.stack, discovery.resources)) {
175
+ const error = errorFactories.stackNotFound(options.stack);
176
+ if (options.json || options.jsonLegacy)
177
+ throw error;
178
+ error.exit();
179
+ }
172
180
  const savedPlan = readSavedHostPortPlan(projectRoot);
173
181
  const hostPortPlan = savedPlan ?? (await createHostPortPlan());
174
182
  const httpPort = hostPortPlan.ingressHttp;
175
- const discovery = createDiscoveryContext();
176
183
  const baseDomain = determineDefaultDomain();
177
184
  const bareDomain = baseDomain.replace(/^(app|api)\./, "");
178
185
  const appDomain = `app.${bareDomain}`;
@@ -1,10 +1,11 @@
1
1
  import chalk from "chalk";
2
2
  import { Command } from "commander";
3
3
  import { createDiscoveryContext } from "../utils/discovery-context.js";
4
- import { requireProjectRoot, runCommand } from "../utils/errors.js";
4
+ import { errorFactories, requireProjectRoot, runCommand } from "../utils/errors.js";
5
5
  import { formatCount, showAllSatisfyCondition, showDetail, showEmptyState, showStep, } from "../utils/formatting.js";
6
6
  import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
7
7
  import { findProjectRoot } from "../utils/paths.js";
8
+ import { stackExists } from "../utils/services.js";
8
9
  export const resourcesCommand = new Command("resources")
9
10
  .description("List all resources (services) in the project")
10
11
  .option("-v, --verbose", "Show detailed information about each resource", false)
@@ -23,6 +24,12 @@ export const resourcesCommand = new Command("resources")
23
24
  requireProjectRoot();
24
25
  }
25
26
  const discovery = createDiscoveryContext();
27
+ if (options.stack && !stackExists(options.stack, discovery.resources)) {
28
+ const error = errorFactories.stackNotFound(options.stack);
29
+ if (options.json)
30
+ writeMachineError(error);
31
+ error.exit();
32
+ }
26
33
  const withoutStackFilter = options.noStack || options.stack === false;
27
34
  let resources = discovery.resources;
28
35
  if (options.stack) {
@@ -10,11 +10,21 @@ import { createStatusMessageController } from "../utils/status-message.js";
10
10
  import { isTiltAvailable } from "../utils/tilt.js";
11
11
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
12
12
  const HelpPanel = ({ onClose }) => (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keyboard Shortcuts" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, underline: true, children: "Navigation" }), _jsx(Text, { children: " \u2191/\u2193 Navigate list items" }), _jsx(Text, { children: " Enter Select item / Open detail" }), _jsx(Text, { children: " Space Toggle expand (tree view)" }), _jsx(Text, { children: " Tab Next tab" }), _jsx(Text, { children: " 1-5 Direct tab access" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "Actions" }) }), _jsx(Text, { children: " m Toggle mouse support" }), _jsx(Text, { children: " t Toggle tooltips" }), _jsx(Text, { children: " e Toggle enabled/disabled services" }), _jsx(Text, { children: " r Refresh data" }), _jsx(Text, { children: " / Search/filter" }), _jsx(Text, { children: " ? Show this help" }), _jsx(Text, { children: " q Quit \u2502 Esc Back" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to close..." }) })] }));
13
- const LoadingScreen = ({ progress, message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { children: ["Loading: ", message] }), _jsxs(Box, { marginY: 1, borderStyle: "single", borderColor: "gray", width: 50, children: [_jsx(Box, { width: progress / 2, backgroundColor: "cyan", children: _jsx(Text, { children: " ".repeat(progress / 2) }) }), _jsxs(Text, { children: [" ", progress, "%"] })] })] }));
13
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
14
+ export const LoadingScreen = ({ message = "Discovering resources...", animated = true, }) => {
15
+ const [frame, setFrame] = useState(0);
16
+ useEffect(() => {
17
+ if (!animated)
18
+ return;
19
+ const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80);
20
+ return () => clearInterval(timer);
21
+ }, [animated]);
22
+ return (_jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "cyan", children: "\u2593\u2592\u2591 TDK NEON EDITION \u2591\u2592\u2593" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { children: [animated && _jsxs(Text, { color: "cyan", children: [SPINNER_FRAMES[frame], " "] }), message] })] }));
23
+ };
14
24
  // biome-ignore lint/correctness/noUnusedFunctionParameters: reserved callback prop kept in the component API
15
25
  const ErrorScreen = ({ error, onRetry }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "red", children: "Could Not Load Services" }), _jsx(Box, { marginY: 1 }), _jsxs(Text, { color: "red", children: ["\u2717 ", error] }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "Troubleshooting:" }), _jsx(Text, { color: "gray", children: " 1. Check that every service.json is valid JSON" }), _jsx(Text, { color: "gray", children: " 2. Run tdk from your project (the folder with the Tiltfile)" }), _jsx(Text, { color: "gray", children: " 3. Try: tdk status --verbose" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to retry or [q] to quit" })] }));
16
26
  const EmptyState = ({ message }) => (_jsxs(Box, { flexDirection: "column", padding: 2, alignItems: "center", children: [_jsx(Text, { bold: true, color: "yellow", children: "No Services Found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "gray", children: "\u25C9 No service.json files found" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { children: "To get started:" }), _jsx(Text, { children: " 1. Run: tdk project" }), _jsx(Text, { children: " 2. Run: tdk resource api --type backend" }), _jsx(Box, { marginY: 1 }), _jsx(Text, { color: "cyan", children: "Press [r] to refresh or [q] to quit" }), message && (_jsx(Box, { paddingX: 1, height: 1, children: _jsxs(Text, { color: "cyan", children: ["\u2593\u2592\u2591 ", message, " \u2591\u2592\u2593"] }) }))] }));
17
- const TUIApp = () => {
27
+ const TUIApp = ({ animated = true }) => {
18
28
  const { exit } = useApp();
19
29
  const { stdout } = useStdout();
20
30
  const { stdin, setRawMode } = useStdin();
@@ -401,7 +411,7 @@ const TUIApp = () => {
401
411
  const compactTabBar = terminalWidth < 100;
402
412
  const compact = terminalWidth < 80;
403
413
  if (loading) {
404
- return _jsx(LoadingScreen, { progress: 100, message: "Initializing..." });
414
+ return _jsx(LoadingScreen, { message: "Discovering resources...", animated: animated });
405
415
  }
406
416
  if (error) {
407
417
  return _jsx(ErrorScreen, { error: error, onRetry: refresh });
@@ -431,12 +441,12 @@ export const uiCommand = new Command("ui")
431
441
  .option("-v, --verbose", "Enable verbose output", false)
432
442
  .option("--no-animations", "Disable animations")
433
443
  .option("--high-contrast", "Enable high contrast mode")
434
- .action(async () => {
444
+ .action(async (options) => {
435
445
  const tiltAvailable = await isTiltAvailable();
436
446
  if (!tiltAvailable) {
437
447
  errorFactories.tiltNotInstalled().exit();
438
448
  }
439
449
  requireProjectRoot();
440
- render(_jsx(TUIApp, {}));
450
+ render(_jsx(TUIApp, { animated: options.animations }));
441
451
  });
442
452
  //# sourceMappingURL=ui.js.map
@@ -2,7 +2,7 @@ import { connect } from "node:net";
2
2
  import { join } from "node:path";
3
3
  import chalk from "chalk";
4
4
  import { Command } from "commander";
5
- import { ensureProjectRuntimeAssets } from "../generator/template-engine.js";
5
+ import { ensureProjectRuntimeAssets, ProjectConfigNotFoundError, verifyMasterConfigs, } from "../generator/template-engine.js";
6
6
  import { DEVCONTAINER_DOCKER_FIX, detectHost, isContainerHost, WEBCONTAINER_UP_MESSAGE, } from "../utils/agent-host.js";
7
7
  import { handleDryRun } from "../utils/command-helpers.js";
8
8
  import { getDeferredResourceNames } from "../utils/doctor-runtime.js";
@@ -54,6 +54,48 @@ async function defaultDockerReachable() {
54
54
  execFile("docker", ["ps"], { timeout: 10_000 }, (error) => resolve(!error));
55
55
  });
56
56
  }
57
+ /** Exit status for `tdk up` when generated files no longer match service.json / project.json. */
58
+ export const DRIFT_EXIT_CODE = 2;
59
+ /**
60
+ * Generated service files edited by hand. Only these block `tdk up`: when service.json itself changed, `tdk up` regenerates
61
+ * the outputs, so stale outputs are the normal edit-then-up loop and not drift.
62
+ */
63
+ export function driftReport(projectRoot) {
64
+ let handEdited;
65
+ try {
66
+ ({ handEdited } = verifyMasterConfigs(projectRoot));
67
+ }
68
+ catch (error) {
69
+ // A project with no .tdk/project.json has no snapshots to compare. Anything else (a corrupt config) must not skip the gate.
70
+ if (error instanceof ProjectConfigNotFoundError)
71
+ return null;
72
+ throw error;
73
+ }
74
+ if (handEdited.length === 0)
75
+ return null;
76
+ return [
77
+ "Generated files were edited by hand and no longer match service.json:",
78
+ ...handEdited.map((file) => ` ${file}`),
79
+ "Run `tdk config regenerate` to discard the edits, or bypass with `tdk up --ignore-drift`.",
80
+ ];
81
+ }
82
+ /**
83
+ * Runs before Tilt is started: exits with DRIFT_EXIT_CODE on drift unless `ignoreDrift` is set. With `ignoreDrift` the
84
+ * warning is printed every time, whether or not anything drifted, because the check is skipped and cannot tell.
85
+ */
86
+ export function enforceDriftGate(projectRoot, options, exit = process.exit) {
87
+ if (options.ignoreDrift) {
88
+ console.warn(chalk.yellow("Warning: --ignore-drift set; generated files were not checked."));
89
+ return;
90
+ }
91
+ const drift = driftReport(projectRoot);
92
+ if (!drift)
93
+ return;
94
+ options.onDrift?.(drift.join("\n"));
95
+ for (const line of drift)
96
+ console.error(chalk.red(line));
97
+ exit(DRIFT_EXIT_CODE);
98
+ }
57
99
  function waitForTiltUi(port, timeoutMs = 30_000) {
58
100
  return new Promise((resolve) => {
59
101
  const deadline = Date.now() + timeoutMs;
@@ -84,6 +126,7 @@ export const upCommand = new Command("up")
84
126
  .option("-q, --quiet", "Suppress non-essential output", false)
85
127
  .option("--dry-run", "Show what would be started without starting", false)
86
128
  .option("-f, --force", "Kill existing Tilt process before starting", false)
129
+ .option("--ignore-drift", "Start even if generated files differ from service.json", false)
87
130
  .option("--only <services...>", "Start only these services plus what the Tiltfile enables for them (their dependsOn services and shared infrastructure). With a stack, names must belong to that stack; dependencies may cross stacks")
88
131
  .option("--json", "Print one JSON object on stdout when the stack is ready or the command fails; implies --quiet", false)
89
132
  .action(async (stackName, options) => {
@@ -109,9 +152,8 @@ export const upCommand = new Command("up")
109
152
  await assertMachineReadyOrExit();
110
153
  }
111
154
  const action = async () => {
112
- const projectRoot = options.dryRun
113
- ? requireProjectRoot()
114
- : (findProjectRoot() ?? process.cwd());
155
+ const foundRoot = options.dryRun ? requireProjectRoot() : findProjectRoot();
156
+ const projectRoot = foundRoot ?? process.cwd();
115
157
  const discoveredResources = discoverResourcesStrict();
116
158
  // Reject a bad request before anything below can write to the project (.env, runtime assets, .tdk/project.json).
117
159
  if (options.only) {
@@ -127,13 +169,20 @@ export const upCommand = new Command("up")
127
169
  showErrorAndExit(message, 2);
128
170
  }
129
171
  }
172
+ // Also under --dry-run: the check only reads, and a dry run should show what a real run would refuse.
173
+ if (foundRoot) {
174
+ enforceDriftGate(foundRoot, {
175
+ ignoreDrift: options.ignoreDrift,
176
+ onDrift: (message) => emit?.({ ok: false }, [{ code: "DRIFT_DETECTED", message }]),
177
+ });
178
+ }
130
179
  const hostPortPlan = await getHostPortPlan(projectRoot, {
131
180
  inspectDocker: !options.dryRun,
132
181
  });
133
182
  if (!options.dryRun) {
134
183
  // An older project's .env predates keys such as JWT_SECRET, which Compose now requires. Add what is missing (never
135
184
  // changing an existing value) before anything starts. Only inside a real project, so a stray run never writes a .env.
136
- if (findProjectRoot()) {
185
+ if (foundRoot) {
137
186
  const addedEnvKeys = completeEnvFile(projectRoot);
138
187
  if (addedEnvKeys.length > 0 && !options.quiet) {
139
188
  console.log(chalk.gray(`Added ${addedEnvKeys.join(", ")} to .env (generated; existing values unchanged)`));
@@ -349,10 +349,13 @@ function normalizeProjectConfig(value) {
349
349
  overrides: config.overrides,
350
350
  };
351
351
  }
352
+ /** The project has no .tdk/project.json yet, as opposed to one that exists but is unreadable or invalid. */
353
+ export class ProjectConfigNotFoundError extends Error {
354
+ }
352
355
  export function readProjectConfig(projectRoot) {
353
356
  const projectJsonPath = path.join(projectRoot, ".tdk", "project.json");
354
357
  if (!fs.existsSync(projectJsonPath)) {
355
- throw new Error(`Project config not found: ${projectJsonPath}. Run 'tdk project --yes' first.`);
358
+ throw new ProjectConfigNotFoundError(`Project config not found: ${projectJsonPath}. Run 'tdk project --yes' first.`);
356
359
  }
357
360
  const jsonContent = fs.readFileSync(projectJsonPath, "utf-8");
358
361
  const parsed = JSON.parse(jsonContent);
@@ -604,6 +607,7 @@ export function verifyMasterConfigs(projectRoot) {
604
607
  const errors = [];
605
608
  const warnings = [];
606
609
  const diffs = [];
610
+ const handEdited = [];
607
611
  const addDrift = (displayPath, actual, expected) => {
608
612
  if (actual === expected)
609
613
  return;
@@ -629,9 +633,11 @@ export function verifyMasterConfigs(projectRoot) {
629
633
  const validation = validateServiceManifestFile(manifestPath, displayPath);
630
634
  errors.push(...validation.errors);
631
635
  warnings.push(...validation.warnings);
632
- errors.push(...verifyGeneratedResourceFiles(projectRoot, manifestPath));
636
+ const generated = verifyGeneratedResourceFiles(projectRoot, manifestPath);
637
+ errors.push(...generated.errors);
638
+ handEdited.push(...generated.handEdited);
633
639
  }
634
- return { valid: errors.length === 0, errors, warnings, diffs };
640
+ return { valid: errors.length === 0, errors, warnings, diffs, handEdited };
635
641
  }
636
642
  function createUnifiedDiff(file, actual, expected) {
637
643
  const oldLines = actual.replace(/\n$/, "").split("\n");
@@ -651,7 +657,7 @@ function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
651
657
  const serviceDir = path.dirname(serviceJsonPath);
652
658
  const generatedDir = path.join(serviceDir, ".autogenerated");
653
659
  if (!fs.existsSync(generatedDir))
654
- return [];
660
+ return { errors: [], handEdited: [] };
655
661
  const generatedFiles = [];
656
662
  const collect = (directory, relativeDir = "") => {
657
663
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
@@ -667,10 +673,11 @@ function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
667
673
  };
668
674
  collect(generatedDir);
669
675
  if (generatedFiles.length === 0)
670
- return [];
676
+ return { errors: [], handEdited: [] };
671
677
  const contractDir = path.join(generatedDir, ".tdk-contract");
672
678
  const sourceSnapshot = path.join(contractDir, "service.json.source");
673
679
  const errors = [];
680
+ const handEdited = [];
674
681
  if (!fs.existsSync(sourceSnapshot)) {
675
682
  errors.push(path.relative(projectRoot, sourceSnapshot) +
676
683
  ": generated-file contract snapshot is missing (run tdk up to regenerate)");
@@ -691,9 +698,11 @@ function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
691
698
  errors.push(`${display}: generated-file snapshot is missing (run tdk up to regenerate)`);
692
699
  }
693
700
  else if (fs.readFileSync(generated.fullPath, "utf-8") !== fs.readFileSync(snapshotPath, "utf-8")) {
701
+ // Windows path.relative uses backslashes; report project-relative paths with forward slashes everywhere.
702
+ handEdited.push(display.split(path.sep).join("/"));
694
703
  errors.push(`${display}: generated file differs from its TDK snapshot (run tdk up to regenerate)`);
695
704
  }
696
705
  }
697
- return errors;
706
+ return { errors, handEdited };
698
707
  }
699
708
  //# sourceMappingURL=template-engine.js.map
@@ -204,9 +204,9 @@ export function getResourcesForStack(stackName) {
204
204
  const allResources = discoverResources();
205
205
  return allResources.filter((r) => r.stack === stackName);
206
206
  }
207
- export function stackExists(stackName) {
208
- const resources = getResourcesForStack(stackName);
209
- return resources.length > 0;
207
+ export function stackExists(stackName, resources) {
208
+ const resourcesToSearch = resources ?? discoverResources();
209
+ return resourcesToSearch.some((resource) => resource.stack === stackName);
210
210
  }
211
211
  /**
212
212
  * Cache TTL in milliseconds
package/cli/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tdk-cli-internal",
3
3
  "private": true,
4
- "version": "1.3.107",
4
+ "version": "1.3.109",
5
5
  "description": "TDK CLI — start services on your laptop.",
6
6
  "keywords": [
7
7
  "tdk",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdk-landscape/tdk-cli-core",
3
- "version": "1.3.107",
3
+ "version": "1.3.109",
4
4
  "description": "TDK CLI — start services on your laptop.",
5
5
  "homepage": "https://tdk-landscape.github.io/tdk-website",
6
6
  "repository": {