@hanmariyang/drafting 1.6.2
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/LICENSE +21 -0
- package/README.md +217 -0
- package/api/dist/db/index.js +107 -0
- package/api/dist/db/repos.js +670 -0
- package/api/dist/index.js +89 -0
- package/api/dist/lib/ai.js +314 -0
- package/api/dist/lib/config.js +57 -0
- package/api/dist/lib/crypto.js +71 -0
- package/api/dist/lib/design-system-gen.js +332 -0
- package/api/dist/lib/fixtures.js +150 -0
- package/api/dist/lib/gateway.js +55 -0
- package/api/dist/lib/handoff.js +283 -0
- package/api/dist/lib/items-gen.js +211 -0
- package/api/dist/lib/lint-service.js +118 -0
- package/api/dist/lib/lint.js +141 -0
- package/api/dist/lib/mockup-gen.js +136 -0
- package/api/dist/lib/numbering.js +75 -0
- package/api/dist/lib/provider-errors.js +31 -0
- package/api/dist/lib/render.js +154 -0
- package/api/dist/lib/style-guide.js +47 -0
- package/api/dist/lib/templates.js +85 -0
- package/api/dist/lib/types.js +1 -0
- package/api/dist/lib/wireframes.js +137 -0
- package/api/dist/providers/byok/anthropic.js +75 -0
- package/api/dist/providers/byok/openai-compat.js +95 -0
- package/api/dist/providers/cli.js +391 -0
- package/api/dist/providers/index.js +68 -0
- package/api/dist/providers/managed.js +22 -0
- package/api/dist/providers/sse.js +37 -0
- package/api/dist/providers/stub.js +55 -0
- package/api/dist/providers/types.js +1 -0
- package/api/dist/routes/backup.js +24 -0
- package/api/dist/routes/deliverables.js +588 -0
- package/api/dist/routes/documents.js +205 -0
- package/api/dist/routes/helpers.js +43 -0
- package/api/dist/routes/interview.js +141 -0
- package/api/dist/routes/keys.js +70 -0
- package/api/dist/routes/projects.js +103 -0
- package/api/dist/routes/settings.js +134 -0
- package/api/dist/routes/share.js +39 -0
- package/api/dist/routes/suggestions.js +144 -0
- package/api/templates/design-system.json +17 -0
- package/api/templates/feature-spec.json +73 -0
- package/api/templates/ia.json +52 -0
- package/api/templates/prd.json +60 -0
- package/api/templates/user-flow.json +61 -0
- package/bin/drafting.mjs +79 -0
- package/db/schema.sql +154 -0
- package/package.json +62 -0
- package/web/dist/assets/index-CS06cWP3.js +125 -0
- package/web/dist/assets/index-DWoYeaZU.css +1 -0
- package/web/dist/index.html +14 -0
package/bin/drafting.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* drafting — 터미널에서 데몬을 띄우고 브라우저를 연다.
|
|
4
|
+
* 서버가 "터미널 프로세스" 컨텍스트로 돌기 때문에:
|
|
5
|
+
* · 구독(CLI): 이 터미널의 claude 로그인/키체인을 그대로 사용 → GUI 앱과 달리 막히지 않는다.
|
|
6
|
+
* · BYOK(env): ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / LITELLM_API_KEY 자동 인식(마법사 불필요).
|
|
7
|
+
* 사용: drafting serve [--port 8477] [--no-open]
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { randomBytes } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const cmd = args[0] && !args[0].startsWith('-') ? args[0] : 'serve';
|
|
18
|
+
if (cmd === 'help' || cmd === '--help' || (cmd !== 'serve')) {
|
|
19
|
+
console.log('사용법: drafting serve [--port 8477] [--no-open]');
|
|
20
|
+
process.exit(cmd === 'help' || cmd === '--help' ? 0 : 1);
|
|
21
|
+
}
|
|
22
|
+
const flag = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
|
|
23
|
+
const noOpen = args.includes('--no-open');
|
|
24
|
+
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const pkgRoot = join(here, '..'); // 설치본 루트 — db/schema.sql·web/dist·api/templates 해석 기준
|
|
27
|
+
const entry = join(pkgRoot, 'api', 'dist', 'index.js');
|
|
28
|
+
if (!existsSync(entry)) {
|
|
29
|
+
console.error(`빌드 산출물이 없습니다: ${entry}\n먼저 'npm run build' 하세요.`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const port = flag('--port', process.env.PORT || '8477');
|
|
33
|
+
const dataDir = process.env.DRAFTING_HOME || join(homedir(), '.drafting');
|
|
34
|
+
mkdirSync(dataDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// APP_ENCRYPTION_KEY 자동 생성·보관 (사용자가 관리하지 않게). env 로 주면 그걸 우선.
|
|
37
|
+
let encKey = process.env.APP_ENCRYPTION_KEY;
|
|
38
|
+
if (!encKey) {
|
|
39
|
+
const p = join(dataDir, '.enckey');
|
|
40
|
+
if (existsSync(p)) encKey = readFileSync(p, 'utf8').trim();
|
|
41
|
+
else { encKey = randomBytes(32).toString('base64'); writeFileSync(p, encKey, { mode: 0o600 }); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const env = {
|
|
45
|
+
...process.env,
|
|
46
|
+
DRAFTING_ROOT: process.env.DRAFTING_ROOT || pkgRoot,
|
|
47
|
+
PORT: port,
|
|
48
|
+
HOST: process.env.HOST || '127.0.0.1',
|
|
49
|
+
DATABASE_PATH: process.env.DATABASE_PATH || join(dataDir, 'drafting.sqlite'),
|
|
50
|
+
APP_ENCRYPTION_KEY: encKey,
|
|
51
|
+
};
|
|
52
|
+
const url = `http://127.0.0.1:${port}`;
|
|
53
|
+
const envKeys = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'LITELLM_API_KEY'].filter((k) => process.env[k]);
|
|
54
|
+
console.log('\n Drafting — 터미널 모드');
|
|
55
|
+
console.log(` · 데이터 ${env.DATABASE_PATH}`);
|
|
56
|
+
console.log(` · 구독(CLI) 이 터미널의 claude 로그인/키체인 사용`);
|
|
57
|
+
console.log(` · BYOK(env) ${envKeys.length ? envKeys.join(', ') : '없음 (설정에서 등록하거나 env 로 주입)'}`);
|
|
58
|
+
console.log(` · 주소 ${url}\n`);
|
|
59
|
+
|
|
60
|
+
const child = spawn(process.execPath, [entry], { env, stdio: 'inherit' });
|
|
61
|
+
|
|
62
|
+
if (!noOpen) {
|
|
63
|
+
const openBrowser = (u) => {
|
|
64
|
+
const c = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
65
|
+
const a = process.platform === 'win32' ? ['/c', 'start', '', u] : [u];
|
|
66
|
+
spawn(c, a, { stdio: 'ignore', detached: true }).unref();
|
|
67
|
+
};
|
|
68
|
+
const t0 = Date.now();
|
|
69
|
+
const poll = setInterval(async () => {
|
|
70
|
+
try {
|
|
71
|
+
const r = await fetch(`${url}/api/health`);
|
|
72
|
+
if (r.ok) { clearInterval(poll); openBrowser(url); }
|
|
73
|
+
} catch { /* not up yet */ }
|
|
74
|
+
if (Date.now() - t0 > 15000) clearInterval(poll);
|
|
75
|
+
}, 400);
|
|
76
|
+
}
|
|
77
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
78
|
+
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
79
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
package/db/schema.sql
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
-- PlanForge schema (SQLite / node:sqlite). Applied idempotently on boot.
|
|
2
|
+
-- Data model: Project · Document · Section · InterviewSession (+ supporting tables).
|
|
3
|
+
|
|
4
|
+
PRAGMA journal_mode = WAL;
|
|
5
|
+
PRAGMA foreign_keys = ON;
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
name TEXT NOT NULL,
|
|
10
|
+
description TEXT NOT NULL DEFAULT '',
|
|
11
|
+
created_at TEXT NOT NULL,
|
|
12
|
+
updated_at TEXT NOT NULL
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
-- Documents form a chain: PRD -> feature-spec -> ia -> user-flow (parent_document_id).
|
|
16
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
19
|
+
type TEXT NOT NULL, -- prd | feature-spec | ia | user-flow
|
|
20
|
+
title TEXT NOT NULL,
|
|
21
|
+
status TEXT NOT NULL DEFAULT 'draft', -- draft | streaming | ready
|
|
22
|
+
parent_document_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
|
|
23
|
+
version INTEGER NOT NULL DEFAULT 0, -- bumps on save/regenerate/restore
|
|
24
|
+
context_stale INTEGER NOT NULL DEFAULT 0, -- 1 = parent changed, needs refresh (P-01)
|
|
25
|
+
context_source_version INTEGER, -- parent version last inherited
|
|
26
|
+
context_pending_version INTEGER, -- parent version awaiting inheritance
|
|
27
|
+
created_at TEXT NOT NULL,
|
|
28
|
+
updated_at TEXT NOT NULL
|
|
29
|
+
);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_documents_project ON documents(project_id);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_documents_parent ON documents(parent_document_id);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS sections (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
36
|
+
position INTEGER NOT NULL,
|
|
37
|
+
heading TEXT NOT NULL DEFAULT '',
|
|
38
|
+
body TEXT NOT NULL DEFAULT '',
|
|
39
|
+
-- AI-authored content is always a PROPOSAL until accepted (SYSTEM.md §0.1).
|
|
40
|
+
-- Only 'accepted' sections are documents / appear in exports & shares (§0.2).
|
|
41
|
+
status TEXT NOT NULL DEFAULT 'proposed', -- proposed | accepted | rejected
|
|
42
|
+
created_at TEXT NOT NULL,
|
|
43
|
+
updated_at TEXT NOT NULL
|
|
44
|
+
);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_sections_document ON sections(document_id);
|
|
46
|
+
|
|
47
|
+
-- AI suggestions (proposals). Every proposal carries a source/basis (§0.3) and
|
|
48
|
+
-- is handled 3 ways: accept / reject / rewrite (§0.4). A suggestion may target a
|
|
49
|
+
-- section, or the whole document (section_id NULL — e.g. a "missing" question).
|
|
50
|
+
CREATE TABLE IF NOT EXISTS suggestions (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
53
|
+
section_id TEXT REFERENCES sections(id) ON DELETE CASCADE, -- nullable = document-level
|
|
54
|
+
-- Structure-doc suggestions target a plan_item instead of a section (v0.4+).
|
|
55
|
+
-- Forward-references plan_items (created below); SQLite resolves at insert time.
|
|
56
|
+
target_item_id TEXT REFERENCES plan_items(id) ON DELETE CASCADE,
|
|
57
|
+
kind TEXT NOT NULL, -- add | revise | delete | question | stale | lint
|
|
58
|
+
title TEXT NOT NULL DEFAULT '',
|
|
59
|
+
body TEXT NOT NULL DEFAULT '', -- human explanation
|
|
60
|
+
quote_before TEXT NOT NULL DEFAULT '', -- current text (revise/delete)
|
|
61
|
+
quote_after TEXT NOT NULL DEFAULT '', -- proposed text (add/revise)
|
|
62
|
+
source TEXT NOT NULL DEFAULT '', -- basis: "Q3" | "PRD §2" | lint code "W-ORPHAN-SPEC"
|
|
63
|
+
status TEXT NOT NULL DEFAULT 'open', -- open | accepted | rejected | dismissed
|
|
64
|
+
created_at TEXT NOT NULL,
|
|
65
|
+
resolved_at TEXT
|
|
66
|
+
);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_suggestions_document ON suggestions(document_id);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_suggestions_section ON suggestions(section_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_suggestions_status ON suggestions(document_id, status);
|
|
70
|
+
|
|
71
|
+
-- Plan items — the rows of the STRUCTURE documents (feature-spec · IA · user-flow).
|
|
72
|
+
-- They coexist with sections (which back the prose PRD). AI never assigns ref_id
|
|
73
|
+
-- (server-numbered, §1.2); AI output is always 'proposed' until accepted (§0.1).
|
|
74
|
+
CREATE TABLE IF NOT EXISTS plan_items (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
77
|
+
parent_id TEXT REFERENCES plan_items(id) ON DELETE CASCADE,
|
|
78
|
+
kind TEXT NOT NULL, -- feature-group|feature|page|flow|step
|
|
79
|
+
ref_id TEXT NOT NULL, -- F-01|F-01-3|PG-01|FLOW-01|FLOW-01.2 (server)
|
|
80
|
+
position INTEGER NOT NULL,
|
|
81
|
+
title TEXT NOT NULL,
|
|
82
|
+
body TEXT NOT NULL DEFAULT '', -- acceptance criteria etc (newline-separated)
|
|
83
|
+
meta TEXT NOT NULL DEFAULT '{}', -- JSON: links/priority/page_type/branch (§1.1)
|
|
84
|
+
status TEXT NOT NULL DEFAULT 'proposed', -- proposed|accepted|rejected
|
|
85
|
+
created_at TEXT NOT NULL,
|
|
86
|
+
updated_at TEXT NOT NULL
|
|
87
|
+
);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_plan_items_document ON plan_items(document_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_plan_items_parent ON plan_items(parent_id);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS interview_sessions (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
94
|
+
template_id TEXT NOT NULL,
|
|
95
|
+
status TEXT NOT NULL DEFAULT 'active', -- active | complete
|
|
96
|
+
current_index INTEGER NOT NULL DEFAULT 0,
|
|
97
|
+
answers TEXT NOT NULL DEFAULT '[]', -- JSON [{questionId, question, answer}]
|
|
98
|
+
created_at TEXT NOT NULL,
|
|
99
|
+
updated_at TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_document ON interview_sessions(document_id);
|
|
102
|
+
|
|
103
|
+
-- BYOK keys — stored ENCRYPTED only (AES-256-GCM). Never plaintext (G-01).
|
|
104
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
105
|
+
id TEXT PRIMARY KEY,
|
|
106
|
+
provider TEXT NOT NULL UNIQUE, -- anthropic | openai | openrouter
|
|
107
|
+
label TEXT NOT NULL DEFAULT '',
|
|
108
|
+
ciphertext TEXT NOT NULL, -- base64
|
|
109
|
+
iv TEXT NOT NULL, -- base64
|
|
110
|
+
auth_tag TEXT NOT NULL, -- base64
|
|
111
|
+
last4 TEXT NOT NULL DEFAULT '',
|
|
112
|
+
created_at TEXT NOT NULL,
|
|
113
|
+
updated_at TEXT NOT NULL
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE TABLE IF NOT EXISTS document_versions (
|
|
117
|
+
id TEXT PRIMARY KEY,
|
|
118
|
+
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
119
|
+
version INTEGER NOT NULL,
|
|
120
|
+
event_type TEXT NOT NULL, -- save | context_inherit | restore
|
|
121
|
+
snapshot TEXT NOT NULL, -- JSON {title, sections:[{heading,body,position}]}
|
|
122
|
+
meta TEXT NOT NULL DEFAULT '{}',
|
|
123
|
+
note TEXT NOT NULL DEFAULT '',
|
|
124
|
+
created_at TEXT NOT NULL
|
|
125
|
+
);
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_versions_document ON document_versions(document_id);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS share_links (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
131
|
+
token TEXT NOT NULL UNIQUE,
|
|
132
|
+
expires_at TEXT, -- ISO 8601, NULL = never
|
|
133
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
134
|
+
created_at TEXT NOT NULL
|
|
135
|
+
);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_share_token ON share_links(token);
|
|
137
|
+
|
|
138
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
139
|
+
key TEXT PRIMARY KEY,
|
|
140
|
+
value TEXT NOT NULL, -- JSON
|
|
141
|
+
updated_at TEXT NOT NULL
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
CREATE TABLE IF NOT EXISTS mockups (
|
|
145
|
+
id TEXT PRIMARY KEY,
|
|
146
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
147
|
+
page_ref TEXT NOT NULL, -- IA 페이지 ref_id (PG-nn)
|
|
148
|
+
html TEXT NOT NULL, -- 자기완결 HTML 시안
|
|
149
|
+
status TEXT NOT NULL DEFAULT 'proposed', -- proposed | accepted
|
|
150
|
+
style_key TEXT, -- 생성 시 StyleGuide preset (재생성 판단용)
|
|
151
|
+
created_at TEXT NOT NULL,
|
|
152
|
+
UNIQUE(project_id, page_ref)
|
|
153
|
+
);
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_mockups_project ON mockups(project_id);
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hanmariyang/drafting",
|
|
3
|
+
"version": "1.6.2",
|
|
4
|
+
"description": "AI Planning Workspace — self-hosted, BYOK. Draft PRDs & feature specs via staged AI interviews. Run: npx @hanmariyang/drafting serve",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/hanmariyang/drafting",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hanmariyang/drafting.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"prd",
|
|
13
|
+
"planning",
|
|
14
|
+
"ai",
|
|
15
|
+
"product",
|
|
16
|
+
"self-hosted",
|
|
17
|
+
"byok"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"drafting": "bin/drafting.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"api/dist/",
|
|
25
|
+
"api/templates/",
|
|
26
|
+
"db/schema.sql",
|
|
27
|
+
"web/dist/"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"workspaces": [
|
|
33
|
+
"api",
|
|
34
|
+
"web"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "concurrently -n api,web -c cyan,magenta \"npm:dev:api\" \"npm:dev:web\"",
|
|
38
|
+
"dev:api": "npm run dev --workspace api",
|
|
39
|
+
"dev:web": "npm run dev --workspace web",
|
|
40
|
+
"build": "npm run build --workspace web && npm run build --workspace api",
|
|
41
|
+
"start": "node api/dist/index.js",
|
|
42
|
+
"test": "npm run test --workspace api",
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@fastify/cors": "^10.0.1",
|
|
47
|
+
"@fastify/static": "^8.0.3",
|
|
48
|
+
"fastify": "^5.2.0",
|
|
49
|
+
"marked": "^15.0.4",
|
|
50
|
+
"nanoid": "^5.0.9",
|
|
51
|
+
"zod": "^3.24.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"concurrently": "^9.1.0",
|
|
55
|
+
"gifenc": "^1.0.3",
|
|
56
|
+
"playwright": "^1.62.1",
|
|
57
|
+
"pngjs": "^7.0.0"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=22"
|
|
61
|
+
}
|
|
62
|
+
}
|