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.
- package/AGENTS.md +52 -0
- package/CLAUDE.md +64 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/docs/artifact-schema-v1.md +96 -0
- package/docs/assets/artifacty.png +0 -0
- package/docs/integrations.md +196 -0
- package/docs/release-checklist.md +30 -0
- package/package.json +55 -0
- package/scripts/smoke.sh +104 -0
- package/src/cli.js +348 -0
- package/src/client/editor.js +260 -0
- package/src/lib/backup.js +75 -0
- package/src/lib/check.js +124 -0
- package/src/lib/converters.js +586 -0
- package/src/lib/diff.js +69 -0
- package/src/lib/editor-assets.js +59 -0
- package/src/lib/i18n.js +175 -0
- package/src/lib/installer.js +187 -0
- package/src/lib/render.js +1181 -0
- package/src/lib/security.js +114 -0
- package/src/lib/server-state.js +53 -0
- package/src/lib/service.js +105 -0
- package/src/lib/storage.js +846 -0
- package/src/mcp-server.js +495 -0
- package/src/server.js +576 -0
package/src/lib/i18n.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
export const DEFAULT_LOCALE = "en";
|
|
2
|
+
export const SUPPORTED_LOCALES = ["en", "ko"];
|
|
3
|
+
|
|
4
|
+
const TRANSLATIONS = {
|
|
5
|
+
en: {
|
|
6
|
+
"nav.new": "New",
|
|
7
|
+
"nav.import": "Import",
|
|
8
|
+
"nav.api": "API",
|
|
9
|
+
"nav.index": "Index",
|
|
10
|
+
"nav.cancel": "Cancel",
|
|
11
|
+
"nav.artifact": "Artifact",
|
|
12
|
+
"nav.edit": "Edit",
|
|
13
|
+
"nav.diff": "Diff",
|
|
14
|
+
"nav.raw": "Raw",
|
|
15
|
+
"language.english": "English",
|
|
16
|
+
"language.korean": "Korean",
|
|
17
|
+
"dashboard.count": "{count} artifacts",
|
|
18
|
+
"dashboard.empty": "No artifacts published yet.",
|
|
19
|
+
"filter.search": "Search",
|
|
20
|
+
"filter.tag": "Tag",
|
|
21
|
+
"filter.source": "Source",
|
|
22
|
+
"filter.archived": "Archived",
|
|
23
|
+
"filter.submit": "Filter",
|
|
24
|
+
"filter.clear": "Clear",
|
|
25
|
+
"form.newTitle": "New Artifact",
|
|
26
|
+
"form.editTitle": "Edit {title}",
|
|
27
|
+
"form.title": "Title",
|
|
28
|
+
"form.format": "Format",
|
|
29
|
+
"form.source": "Source",
|
|
30
|
+
"form.type": "Type",
|
|
31
|
+
"form.tags": "Tags",
|
|
32
|
+
"form.content": "Content",
|
|
33
|
+
"form.saveVersion": "Save Version",
|
|
34
|
+
"form.create": "Create",
|
|
35
|
+
"form.importTitle": "Import Artifact",
|
|
36
|
+
"form.agent": "Agent",
|
|
37
|
+
"form.auto": "Auto",
|
|
38
|
+
"form.fileName": "File Name",
|
|
39
|
+
"form.importContent": "Content or JSON Payload",
|
|
40
|
+
"form.importSubmit": "Import",
|
|
41
|
+
"artifact.schema": "schema v{version}",
|
|
42
|
+
"artifact.archived": "archived {date}",
|
|
43
|
+
"artifact.bytes": "{size} bytes",
|
|
44
|
+
"artifact.archive": "Archive",
|
|
45
|
+
"artifact.restore": "Restore",
|
|
46
|
+
"diff.title": "{title} Diff",
|
|
47
|
+
"diff.from": "From",
|
|
48
|
+
"diff.to": "To",
|
|
49
|
+
"diff.line": "Line",
|
|
50
|
+
"diff.compare": "Compare",
|
|
51
|
+
"editor.formatJson": "Format JSON",
|
|
52
|
+
"editor.validJson": "Valid JSON",
|
|
53
|
+
"editor.invalidJson": "Invalid JSON: {message}",
|
|
54
|
+
"editor.htmlPreview": "HTML preview",
|
|
55
|
+
"editor.markdownPreview": "Markdown preview",
|
|
56
|
+
"editor.plainText": "Plain text",
|
|
57
|
+
"editor.mode": "{format} editor"
|
|
58
|
+
},
|
|
59
|
+
ko: {
|
|
60
|
+
"nav.new": "새로 만들기",
|
|
61
|
+
"nav.import": "가져오기",
|
|
62
|
+
"nav.api": "API",
|
|
63
|
+
"nav.index": "목록",
|
|
64
|
+
"nav.cancel": "취소",
|
|
65
|
+
"nav.artifact": "아티팩트",
|
|
66
|
+
"nav.edit": "편집",
|
|
67
|
+
"nav.diff": "비교",
|
|
68
|
+
"nav.raw": "원본",
|
|
69
|
+
"language.english": "영어",
|
|
70
|
+
"language.korean": "한국어",
|
|
71
|
+
"dashboard.count": "아티팩트 {count}개",
|
|
72
|
+
"dashboard.empty": "아직 게시된 아티팩트가 없습니다.",
|
|
73
|
+
"filter.search": "검색",
|
|
74
|
+
"filter.tag": "태그",
|
|
75
|
+
"filter.source": "소스",
|
|
76
|
+
"filter.archived": "보관됨",
|
|
77
|
+
"filter.submit": "필터",
|
|
78
|
+
"filter.clear": "초기화",
|
|
79
|
+
"form.newTitle": "새 아티팩트",
|
|
80
|
+
"form.editTitle": "{title} 편집",
|
|
81
|
+
"form.title": "제목",
|
|
82
|
+
"form.format": "포맷",
|
|
83
|
+
"form.source": "소스",
|
|
84
|
+
"form.type": "유형",
|
|
85
|
+
"form.tags": "태그",
|
|
86
|
+
"form.content": "내용",
|
|
87
|
+
"form.saveVersion": "버전 저장",
|
|
88
|
+
"form.create": "생성",
|
|
89
|
+
"form.importTitle": "아티팩트 가져오기",
|
|
90
|
+
"form.agent": "에이전트",
|
|
91
|
+
"form.auto": "자동",
|
|
92
|
+
"form.fileName": "파일 이름",
|
|
93
|
+
"form.importContent": "내용 또는 JSON 페이로드",
|
|
94
|
+
"form.importSubmit": "가져오기",
|
|
95
|
+
"artifact.schema": "스키마 v{version}",
|
|
96
|
+
"artifact.archived": "보관됨 {date}",
|
|
97
|
+
"artifact.bytes": "{size} 바이트",
|
|
98
|
+
"artifact.archive": "보관",
|
|
99
|
+
"artifact.restore": "복원",
|
|
100
|
+
"diff.title": "{title} 비교",
|
|
101
|
+
"diff.from": "이전",
|
|
102
|
+
"diff.to": "이후",
|
|
103
|
+
"diff.line": "라인",
|
|
104
|
+
"diff.compare": "비교",
|
|
105
|
+
"editor.formatJson": "JSON 정리",
|
|
106
|
+
"editor.validJson": "유효한 JSON",
|
|
107
|
+
"editor.invalidJson": "유효하지 않은 JSON: {message}",
|
|
108
|
+
"editor.htmlPreview": "HTML 미리보기",
|
|
109
|
+
"editor.markdownPreview": "Markdown 미리보기",
|
|
110
|
+
"editor.plainText": "일반 텍스트",
|
|
111
|
+
"editor.mode": "{format} 편집기"
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function resolveLocale(value) {
|
|
116
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
117
|
+
if (SUPPORTED_LOCALES.includes(normalized)) {
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
return DEFAULT_LOCALE;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function localeFromUrl(url) {
|
|
124
|
+
return resolveLocale(url.searchParams.get("lang") || process.env.ARTIFACTY_LOCALE);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function localeFromBodyOrUrl(body = {}, url) {
|
|
128
|
+
return resolveLocale(body.lang || url.searchParams.get("lang") || process.env.ARTIFACTY_LOCALE);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createI18n(locale = DEFAULT_LOCALE) {
|
|
132
|
+
const resolved = resolveLocale(locale);
|
|
133
|
+
const messages = TRANSLATIONS[resolved] || TRANSLATIONS[DEFAULT_LOCALE];
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
locale: resolved,
|
|
137
|
+
t(key, params = {}) {
|
|
138
|
+
const template = messages[key] || TRANSLATIONS[DEFAULT_LOCALE][key] || key;
|
|
139
|
+
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => String(params[name] ?? ""));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function localizedHref(href, locale) {
|
|
145
|
+
const resolved = resolveLocale(locale);
|
|
146
|
+
if (resolved === DEFAULT_LOCALE) {
|
|
147
|
+
return stripLocale(href);
|
|
148
|
+
}
|
|
149
|
+
const url = new URL(href, "http://artifacty.local");
|
|
150
|
+
url.searchParams.set("lang", resolved);
|
|
151
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function switchLocaleHref(currentPath, locale) {
|
|
155
|
+
return localizedHref(currentPath || "/", locale);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function editorMessages(locale = DEFAULT_LOCALE) {
|
|
159
|
+
const { t } = createI18n(locale);
|
|
160
|
+
return {
|
|
161
|
+
formatJson: t("editor.formatJson"),
|
|
162
|
+
validJson: t("editor.validJson"),
|
|
163
|
+
invalidJson: t("editor.invalidJson", { message: "{message}" }),
|
|
164
|
+
htmlPreview: t("editor.htmlPreview"),
|
|
165
|
+
markdownPreview: t("editor.markdownPreview"),
|
|
166
|
+
plainText: t("editor.plainText"),
|
|
167
|
+
mode: t("editor.mode", { format: "{format}" })
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stripLocale(href) {
|
|
172
|
+
const url = new URL(href, "http://artifacty.local");
|
|
173
|
+
url.searchParams.delete("lang");
|
|
174
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
175
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_AGENTS = new Set(["all", "claude", "codex", "gemini"]);
|
|
7
|
+
|
|
8
|
+
export async function installAgent(agent, options = {}) {
|
|
9
|
+
const normalized = normalizeAgent(agent);
|
|
10
|
+
if (!SUPPORTED_AGENTS.has(normalized)) {
|
|
11
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (normalized === "all") {
|
|
15
|
+
const results = [];
|
|
16
|
+
for (const target of ["claude", "codex", "gemini"]) {
|
|
17
|
+
results.push(await installAgent(target, options));
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
agent: "all",
|
|
21
|
+
results
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (normalized === "claude") {
|
|
26
|
+
return installClaude(options);
|
|
27
|
+
}
|
|
28
|
+
if (normalized === "codex") {
|
|
29
|
+
return installCodex(options);
|
|
30
|
+
}
|
|
31
|
+
return installGemini(options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createMcpServerConfig(options = {}) {
|
|
35
|
+
const serverRoot = path.resolve(options.packageDir || options.projectDir || process.cwd());
|
|
36
|
+
const serverPath = path.resolve(options.serverPath || path.join(serverRoot, "src", "mcp-server.js"));
|
|
37
|
+
const env = {};
|
|
38
|
+
|
|
39
|
+
if (options.url || process.env.ARTIFACTY_URL) {
|
|
40
|
+
env.ARTIFACTY_URL = options.url || process.env.ARTIFACTY_URL;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.home || process.env.ARTIFACTY_HOME) {
|
|
44
|
+
env.ARTIFACTY_HOME = path.resolve(options.home || process.env.ARTIFACTY_HOME);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
command: "node",
|
|
49
|
+
args: [serverPath],
|
|
50
|
+
env
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function installClaude(options = {}) {
|
|
55
|
+
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
56
|
+
const targetPath = path.resolve(options.configPath || path.join(projectDir, ".mcp.json"));
|
|
57
|
+
const existing = await readJsonFile(targetPath, {});
|
|
58
|
+
const next = {
|
|
59
|
+
...existing,
|
|
60
|
+
mcpServers: {
|
|
61
|
+
...(existing.mcpServers || {}),
|
|
62
|
+
artifacty: createMcpServerConfig(options)
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return writeInstallFile({
|
|
67
|
+
agent: "claude",
|
|
68
|
+
path: targetPath,
|
|
69
|
+
dryRun: options.dryRun,
|
|
70
|
+
content: `${JSON.stringify(next, null, 2)}\n`
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function installGemini(options = {}) {
|
|
75
|
+
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
76
|
+
const targetPath = path.resolve(options.configPath || path.join(projectDir, ".gemini", "settings.json"));
|
|
77
|
+
const existing = await readJsonFile(targetPath, {});
|
|
78
|
+
const next = {
|
|
79
|
+
...existing,
|
|
80
|
+
mcpServers: {
|
|
81
|
+
...(existing.mcpServers || {}),
|
|
82
|
+
artifacty: {
|
|
83
|
+
...createMcpServerConfig(options),
|
|
84
|
+
timeout: options.timeout || 30000,
|
|
85
|
+
trust: Boolean(options.trust)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return writeInstallFile({
|
|
91
|
+
agent: "gemini",
|
|
92
|
+
path: targetPath,
|
|
93
|
+
dryRun: options.dryRun,
|
|
94
|
+
content: `${JSON.stringify(next, null, 2)}\n`
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function installCodex(options = {}) {
|
|
99
|
+
const targetPath = path.resolve(options.configPath || path.join(homedir(), ".codex", "config.toml"));
|
|
100
|
+
const existing = await readTextFile(targetPath, "");
|
|
101
|
+
const block = codexTomlBlock(createMcpServerConfig(options));
|
|
102
|
+
const next = replaceTomlBlock(existing, "mcp_servers.artifacty", block);
|
|
103
|
+
|
|
104
|
+
return writeInstallFile({
|
|
105
|
+
agent: "codex",
|
|
106
|
+
path: targetPath,
|
|
107
|
+
dryRun: options.dryRun,
|
|
108
|
+
content: next
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function codexTomlBlock(config) {
|
|
113
|
+
const envPairs = Object.entries(config.env || {})
|
|
114
|
+
.map(([key, value]) => `${key} = ${quoteTomlString(value)}`)
|
|
115
|
+
.join(", ");
|
|
116
|
+
|
|
117
|
+
return [
|
|
118
|
+
"[mcp_servers.artifacty]",
|
|
119
|
+
`command = ${quoteTomlString(config.command)}`,
|
|
120
|
+
`args = [${config.args.map(quoteTomlString).join(", ")}]`,
|
|
121
|
+
"startup_timeout_sec = 5.0",
|
|
122
|
+
`env = { ${envPairs} }`,
|
|
123
|
+
""
|
|
124
|
+
].join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function replaceTomlBlock(existing, dottedName, block) {
|
|
128
|
+
const trimmed = existing.trimEnd();
|
|
129
|
+
const pattern = new RegExp(`\\n?\\[${escapeRegExp(dottedName)}\\][\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`);
|
|
130
|
+
if (pattern.test(trimmed)) {
|
|
131
|
+
return `${trimmed.replace(pattern, `\n${block.trimEnd()}`)}\n`;
|
|
132
|
+
}
|
|
133
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${block}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function writeInstallFile({ agent, path: targetPath, dryRun, content }) {
|
|
137
|
+
const current = await readTextFile(targetPath, "");
|
|
138
|
+
const changed = current !== content;
|
|
139
|
+
|
|
140
|
+
if (!dryRun && changed) {
|
|
141
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
142
|
+
await writeFile(targetPath, content, "utf8");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
agent,
|
|
147
|
+
path: targetPath,
|
|
148
|
+
changed,
|
|
149
|
+
dryRun: Boolean(dryRun),
|
|
150
|
+
content
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readJsonFile(filePath, fallback) {
|
|
155
|
+
if (!existsSync(filePath)) {
|
|
156
|
+
return fallback;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const raw = await readTextFile(filePath, "");
|
|
160
|
+
if (!raw.trim()) {
|
|
161
|
+
return fallback;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(raw);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(`Failed to parse ${filePath}: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function readTextFile(filePath, fallback) {
|
|
171
|
+
if (!existsSync(filePath)) {
|
|
172
|
+
return fallback;
|
|
173
|
+
}
|
|
174
|
+
return readFile(filePath, "utf8");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeAgent(agent) {
|
|
178
|
+
return String(agent || "").trim().toLowerCase();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function quoteTomlString(value) {
|
|
182
|
+
return JSON.stringify(String(value));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function escapeRegExp(value) {
|
|
186
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
187
|
+
}
|