artifacty 0.1.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.
@@ -0,0 +1,260 @@
1
+ import { EditorView, basicSetup } from "codemirror";
2
+ import { Compartment } from "@codemirror/state";
3
+ import { html } from "@codemirror/lang-html";
4
+ import { json } from "@codemirror/lang-json";
5
+ import { markdown } from "@codemirror/lang-markdown";
6
+
7
+ const messages = {
8
+ formatJson: "Format JSON",
9
+ validJson: "Valid JSON",
10
+ invalidJson: "Invalid JSON: {message}",
11
+ htmlPreview: "HTML preview",
12
+ markdownPreview: "Markdown preview",
13
+ plainText: "Plain text",
14
+ mode: "{format} editor",
15
+ ...(globalThis.ARTIFACTY_I18N || {})
16
+ };
17
+
18
+ const editorTheme = EditorView.theme({
19
+ "&": {
20
+ minHeight: "52vh",
21
+ border: "1px solid var(--line)",
22
+ borderRadius: "8px",
23
+ background: "var(--panel)"
24
+ },
25
+ ".cm-scroller": {
26
+ fontFamily: '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace',
27
+ fontSize: "13px",
28
+ lineHeight: "1.55"
29
+ },
30
+ ".cm-content": {
31
+ minHeight: "52vh"
32
+ },
33
+ ".cm-gutters": {
34
+ borderTopLeftRadius: "8px",
35
+ borderBottomLeftRadius: "8px"
36
+ }
37
+ });
38
+
39
+ for (const textarea of document.querySelectorAll("textarea[data-artifacty-editor]")) {
40
+ enhanceTextarea(textarea);
41
+ }
42
+
43
+ function enhanceTextarea(textarea) {
44
+ const form = textarea.closest("form");
45
+ const formatSelector = form?.querySelector("select[name='format']");
46
+ const fileNameInput = form?.querySelector("input[name='fileName']");
47
+ const language = new Compartment();
48
+ const shell = document.createElement("div");
49
+ shell.className = "codemirror-shell";
50
+ shell.setAttribute("data-enhanced", "true");
51
+
52
+ const toolbar = document.createElement("div");
53
+ toolbar.className = "editor-toolbar";
54
+
55
+ const status = document.createElement("span");
56
+ status.className = "editor-status";
57
+
58
+ const formatJsonButton = document.createElement("button");
59
+ formatJsonButton.type = "button";
60
+ formatJsonButton.className = "secondary-button";
61
+ formatJsonButton.textContent = messages.formatJson;
62
+
63
+ toolbar.append(formatJsonButton, status);
64
+ textarea.after(toolbar, shell);
65
+ textarea.classList.add("textarea-enhanced");
66
+
67
+ const preview = document.createElement("section");
68
+ preview.className = "editor-preview";
69
+ preview.setAttribute("aria-live", "polite");
70
+ shell.after(preview);
71
+
72
+ const currentFormat = () => detectFormat({
73
+ explicit: formatSelector?.value || textarea.dataset.editorFormat,
74
+ fileName: fileNameInput?.value || "",
75
+ content: view.state.doc.toString()
76
+ });
77
+
78
+ const view = new EditorView({
79
+ doc: textarea.value,
80
+ parent: shell,
81
+ extensions: [
82
+ basicSetup,
83
+ EditorView.lineWrapping,
84
+ editorTheme,
85
+ language.of(languageExtension(detectFormat({
86
+ explicit: formatSelector?.value || textarea.dataset.editorFormat,
87
+ fileName: fileNameInput?.value || "",
88
+ content: textarea.value
89
+ }))),
90
+ EditorView.updateListener.of((update) => {
91
+ if (update.docChanged) {
92
+ textarea.value = update.state.doc.toString();
93
+ updatePreview();
94
+ }
95
+ })
96
+ ]
97
+ });
98
+
99
+ const reconfigure = () => {
100
+ const format = currentFormat();
101
+ view.dispatch({ effects: language.reconfigure(languageExtension(format)) });
102
+ updateToolbar(format);
103
+ updatePreview();
104
+ };
105
+
106
+ formatSelector?.addEventListener("change", reconfigure);
107
+ fileNameInput?.addEventListener("input", reconfigure);
108
+ form?.addEventListener("submit", () => {
109
+ textarea.value = view.state.doc.toString();
110
+ });
111
+
112
+ formatJsonButton.addEventListener("click", () => {
113
+ const content = view.state.doc.toString();
114
+ try {
115
+ const formatted = JSON.stringify(JSON.parse(content), null, 2);
116
+ view.dispatch({
117
+ changes: { from: 0, to: view.state.doc.length, insert: formatted }
118
+ });
119
+ status.textContent = messages.validJson;
120
+ } catch (error) {
121
+ status.textContent = invalidJsonMessage(error);
122
+ }
123
+ });
124
+
125
+ updateToolbar(currentFormat());
126
+ updatePreview();
127
+
128
+ function updateToolbar(format) {
129
+ formatJsonButton.hidden = format !== "json";
130
+ status.textContent = formatLabel(format);
131
+ }
132
+
133
+ function updatePreview() {
134
+ const format = currentFormat();
135
+ const content = view.state.doc.toString();
136
+ preview.replaceChildren();
137
+ preview.dataset.format = format;
138
+
139
+ if (format === "html") {
140
+ const frame = document.createElement("iframe");
141
+ frame.className = "editor-preview-frame";
142
+ frame.setAttribute("sandbox", "allow-scripts allow-forms allow-popups");
143
+ frame.srcdoc = content;
144
+ preview.append(frame);
145
+ status.textContent = messages.htmlPreview;
146
+ return;
147
+ }
148
+
149
+ if (format === "json") {
150
+ const pre = document.createElement("pre");
151
+ try {
152
+ pre.textContent = JSON.stringify(JSON.parse(content), null, 2);
153
+ status.textContent = messages.validJson;
154
+ } catch (error) {
155
+ pre.textContent = content;
156
+ status.textContent = invalidJsonMessage(error);
157
+ }
158
+ preview.append(pre);
159
+ return;
160
+ }
161
+
162
+ if (format === "markdown") {
163
+ const article = document.createElement("article");
164
+ article.className = "artifact-doc";
165
+ article.innerHTML = markdownPreview(content);
166
+ preview.append(article);
167
+ status.textContent = messages.markdownPreview;
168
+ return;
169
+ }
170
+
171
+ const pre = document.createElement("pre");
172
+ pre.textContent = content;
173
+ preview.append(pre);
174
+ status.textContent = messages.plainText;
175
+ }
176
+ }
177
+
178
+ function languageExtension(format) {
179
+ if (format === "html") {
180
+ return html();
181
+ }
182
+ if (format === "json") {
183
+ return json();
184
+ }
185
+ if (format === "markdown") {
186
+ return markdown();
187
+ }
188
+ return [];
189
+ }
190
+
191
+ function detectFormat({ explicit, fileName, content }) {
192
+ if (["markdown", "html", "json", "text"].includes(explicit)) {
193
+ return explicit;
194
+ }
195
+
196
+ const lowerName = String(fileName || "").toLowerCase();
197
+ if (lowerName.endsWith(".html") || lowerName.endsWith(".htm")) {
198
+ return "html";
199
+ }
200
+ if (lowerName.endsWith(".md") || lowerName.endsWith(".markdown")) {
201
+ return "markdown";
202
+ }
203
+ if (lowerName.endsWith(".json")) {
204
+ return "json";
205
+ }
206
+
207
+ const trimmed = String(content || "").trimStart();
208
+ if (trimmed.startsWith("<!doctype") || trimmed.startsWith("<html") || trimmed.startsWith("<")) {
209
+ return "html";
210
+ }
211
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
212
+ return "json";
213
+ }
214
+ if (/^#{1,6}\s/m.test(trimmed)) {
215
+ return "markdown";
216
+ }
217
+ return "text";
218
+ }
219
+
220
+ function formatLabel(format) {
221
+ const label = `${format[0].toUpperCase()}${format.slice(1)}`;
222
+ return messages.mode.replaceAll("{format}", label);
223
+ }
224
+
225
+ function invalidJsonMessage(error) {
226
+ return messages.invalidJson.replaceAll("{message}", error.message);
227
+ }
228
+
229
+ function markdownPreview(content) {
230
+ return String(content || "")
231
+ .split(/\r?\n/)
232
+ .map((line) => {
233
+ if (/^###\s+/.test(line)) {
234
+ return `<h3>${escapeHtml(line.replace(/^###\s+/, ""))}</h3>`;
235
+ }
236
+ if (/^##\s+/.test(line)) {
237
+ return `<h2>${escapeHtml(line.replace(/^##\s+/, ""))}</h2>`;
238
+ }
239
+ if (/^#\s+/.test(line)) {
240
+ return `<h1>${escapeHtml(line.replace(/^#\s+/, ""))}</h1>`;
241
+ }
242
+ if (/^[-*]\s+/.test(line)) {
243
+ return `<p>• ${escapeHtml(line.replace(/^[-*]\s+/, ""))}</p>`;
244
+ }
245
+ if (!line.trim()) {
246
+ return "<br>";
247
+ }
248
+ return `<p>${escapeHtml(line)}</p>`;
249
+ })
250
+ .join("");
251
+ }
252
+
253
+ function escapeHtml(value) {
254
+ return String(value)
255
+ .replaceAll("&", "&amp;")
256
+ .replaceAll("<", "&lt;")
257
+ .replaceAll(">", "&gt;")
258
+ .replaceAll('"', "&quot;")
259
+ .replaceAll("'", "&#39;");
260
+ }
@@ -0,0 +1,75 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createStore, loadIndex, writeIndex } from "./storage.js";
4
+
5
+ export async function exportStore(store = createStore(), outputPath) {
6
+ if (!outputPath) {
7
+ throw new Error("export requires --file <path>");
8
+ }
9
+ const index = await loadIndex(store);
10
+ const artifacts = [];
11
+ for (const artifact of index.artifacts) {
12
+ const versions = [];
13
+ for (const version of artifact.versions) {
14
+ versions.push({
15
+ ...version,
16
+ content: await readFile(path.join(store.home, version.path), "utf8")
17
+ });
18
+ }
19
+ artifacts.push({ ...artifact, versions });
20
+ }
21
+
22
+ const bundle = {
23
+ schemaVersion: 1,
24
+ exportedAt: new Date().toISOString(),
25
+ artifacts
26
+ };
27
+ await mkdir(path.dirname(path.resolve(outputPath)), { recursive: true });
28
+ await writeFile(outputPath, `${JSON.stringify(bundle, null, 2)}\n`, "utf8");
29
+ return {
30
+ path: path.resolve(outputPath),
31
+ artifactCount: artifacts.length,
32
+ exportedAt: bundle.exportedAt
33
+ };
34
+ }
35
+
36
+ export async function importStore(store = createStore(), inputPath) {
37
+ if (!inputPath) {
38
+ throw new Error("import-store requires --file <path>");
39
+ }
40
+ const bundle = JSON.parse(await readFile(inputPath, "utf8"));
41
+ if (!Array.isArray(bundle.artifacts)) {
42
+ throw new Error("Invalid Artifacty backup: artifacts array missing");
43
+ }
44
+
45
+ const index = {
46
+ version: 3,
47
+ artifacts: []
48
+ };
49
+
50
+ for (const artifact of bundle.artifacts) {
51
+ const versions = [];
52
+ for (const version of artifact.versions || []) {
53
+ const cleanVersion = { ...version };
54
+ delete cleanVersion.content;
55
+ const content = version.content || "";
56
+ const absolutePath = path.join(store.home, cleanVersion.path);
57
+ await mkdir(path.dirname(absolutePath), { recursive: true });
58
+ await writeFile(absolutePath, content, "utf8");
59
+ versions.push(cleanVersion);
60
+ }
61
+ index.artifacts.push({ ...artifact, versions });
62
+ }
63
+
64
+ await writeIndex(store, index);
65
+ return {
66
+ path: path.resolve(inputPath),
67
+ artifactCount: index.artifacts.length,
68
+ importedAt: new Date().toISOString()
69
+ };
70
+ }
71
+
72
+ export function defaultBackupPath(store = createStore()) {
73
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
74
+ return path.join(store.home, "backups", `artifacty-${stamp}.json`);
75
+ }
@@ -0,0 +1,124 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+
4
+ export const REQUIRED_MCP_TOOLS = [
5
+ "artifacty_create",
6
+ "artifacty_publish",
7
+ "artifacty_import",
8
+ "artifacty_list",
9
+ "artifacty_get",
10
+ "artifacty_update",
11
+ "artifacty_archive",
12
+ "artifacty_restore",
13
+ "artifacty_audit",
14
+ "artifacty_info"
15
+ ];
16
+
17
+ export async function checkMcpTools(options = {}) {
18
+ const projectDir = path.resolve(options.projectDir || process.cwd());
19
+ const serverPath = path.resolve(options.serverPath || path.join(projectDir, "src", "mcp-server.js"));
20
+ const requiredTools = options.requiredTools || REQUIRED_MCP_TOOLS;
21
+ const timeoutMs = Number(options.timeout || 5000);
22
+ const client = spawnMcpClient({
23
+ serverPath,
24
+ timeoutMs,
25
+ env: {
26
+ ...process.env,
27
+ ...(options.url ? { ARTIFACTY_URL: options.url } : {}),
28
+ ...(options.home ? { ARTIFACTY_HOME: path.resolve(options.home) } : {})
29
+ }
30
+ });
31
+
32
+ try {
33
+ const initialized = await client.request("initialize", {
34
+ protocolVersion: "2025-06-18",
35
+ capabilities: {},
36
+ clientInfo: { name: "artifacty-check", version: "0.1.0" }
37
+ });
38
+ client.notify("notifications/initialized", {});
39
+ const listed = await client.request("tools/list", {});
40
+ const toolNames = (listed.tools || []).map((tool) => tool.name).sort();
41
+ const missingTools = requiredTools.filter((tool) => !toolNames.includes(tool));
42
+
43
+ return {
44
+ ok: missingTools.length === 0,
45
+ serverPath,
46
+ protocolVersion: initialized.protocolVersion,
47
+ toolCount: toolNames.length,
48
+ tools: toolNames,
49
+ missingTools
50
+ };
51
+ } finally {
52
+ client.close();
53
+ }
54
+ }
55
+
56
+ function spawnMcpClient({ serverPath, timeoutMs, env }) {
57
+ const child = spawn(process.execPath, [serverPath], {
58
+ cwd: path.dirname(path.dirname(serverPath)),
59
+ env,
60
+ stdio: ["pipe", "pipe", "pipe"]
61
+ });
62
+ let nextId = 1;
63
+ let buffer = "";
64
+ let stderr = "";
65
+ const pending = new Map();
66
+
67
+ child.stdout.on("data", (chunk) => {
68
+ buffer += chunk.toString("utf8");
69
+ let newline = buffer.indexOf("\n");
70
+ while (newline !== -1) {
71
+ const line = buffer.slice(0, newline);
72
+ buffer = buffer.slice(newline + 1);
73
+ if (line.trim()) {
74
+ const message = JSON.parse(line);
75
+ const entry = pending.get(message.id);
76
+ if (entry) {
77
+ clearTimeout(entry.timer);
78
+ pending.delete(message.id);
79
+ if (message.error) {
80
+ entry.reject(new Error(message.error.message));
81
+ } else {
82
+ entry.resolve(message.result);
83
+ }
84
+ }
85
+ }
86
+ newline = buffer.indexOf("\n");
87
+ }
88
+ });
89
+
90
+ child.stderr.on("data", (chunk) => {
91
+ stderr += chunk.toString("utf8");
92
+ });
93
+
94
+ child.on("exit", (code) => {
95
+ for (const entry of pending.values()) {
96
+ clearTimeout(entry.timer);
97
+ entry.reject(new Error(`MCP server exited with code ${code}: ${stderr.trim()}`));
98
+ }
99
+ pending.clear();
100
+ });
101
+
102
+ return {
103
+ request(method, params) {
104
+ const id = nextId;
105
+ nextId += 1;
106
+ const payload = { jsonrpc: "2.0", id, method, params };
107
+ const promise = new Promise((resolve, reject) => {
108
+ const timer = setTimeout(() => {
109
+ pending.delete(id);
110
+ reject(new Error(`Timed out waiting for MCP response to ${method}`));
111
+ }, timeoutMs);
112
+ pending.set(id, { resolve, reject, timer });
113
+ });
114
+ child.stdin.write(`${JSON.stringify(payload)}\n`);
115
+ return promise;
116
+ },
117
+ notify(method, params) {
118
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
119
+ },
120
+ close() {
121
+ child.kill("SIGTERM");
122
+ }
123
+ };
124
+ }