harness-bujang 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/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/index.js +906 -0
- package/package.json +57 -0
- package/templates/agents/en/architect-team.md +105 -0
- package/templates/agents/en/code-review-team.md +106 -0
- package/templates/agents/en/consultant.md +106 -0
- package/templates/agents/en/db-guard-team.md +94 -0
- package/templates/agents/en/dev-team.md +143 -0
- package/templates/agents/en/director.md +401 -0
- package/templates/agents/en/doc-sync-team.md +92 -0
- package/templates/agents/en/qa-team.md +100 -0
- package/templates/agents/en/security-team.md +99 -0
- package/templates/agents/en/verifier-team.md +97 -0
- package/templates/agents/ko/architect-team.md +110 -0
- package/templates/agents/ko/code-review-team.md +124 -0
- package/templates/agents/ko/consultant.md +106 -0
- package/templates/agents/ko/db-guard-team.md +116 -0
- package/templates/agents/ko/dev-team.md +144 -0
- package/templates/agents/ko/director.md +401 -0
- package/templates/agents/ko/doc-sync-team.md +114 -0
- package/templates/agents/ko/qa-team.md +122 -0
- package/templates/agents/ko/security-team.md +121 -0
- package/templates/agents/ko/verifier-team.md +119 -0
- package/templates/project-template/app/admin/harness/harness-client.tsx +493 -0
- package/templates/project-template/app/admin/harness/page.tsx +27 -0
- package/templates/project-template/app/api/harness/logs/route.ts +111 -0
- package/templates/project-template/app/api/harness/reply/route.ts +45 -0
- package/templates/project-template/lib/harness-db/index.ts +45 -0
- package/templates/project-template/lib/harness-db/sqlite.ts +128 -0
- package/templates/project-template/lib/harness-db/supabase.ts +64 -0
- package/templates/project-template/lib/harness-db/types.ts +35 -0
- package/templates/project-template/migrations/00010_harness_messages.sql +56 -0
- package/templates/project-template/migrations/00025_harness_insert_admin_only.sql +17 -0
- package/templates/templates/en/AGENT_LEARNING_LOG.seed.md +43 -0
- package/templates/templates/en/CLAUDE.md.harness-section.template +59 -0
- package/templates/templates/ko/AGENT_LEARNING_LOG.seed.md +43 -0
- package/templates/templates/ko/CLAUDE.md.harness-section.template +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/init.ts
|
|
4
|
+
import * as fs2 from "fs/promises";
|
|
5
|
+
import * as path2 from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
// src/scan.ts
|
|
9
|
+
import * as fs from "fs/promises";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
async function exists(p) {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(p);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function readJson(p) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(await fs.readFile(p, "utf8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function scanProject(target) {
|
|
28
|
+
const has = (p) => exists(path.join(target, p));
|
|
29
|
+
const pkg = await readJson(path.join(target, "package.json"));
|
|
30
|
+
const deps = {
|
|
31
|
+
...pkg?.dependencies,
|
|
32
|
+
...pkg?.devDependencies
|
|
33
|
+
};
|
|
34
|
+
let framework = "Generic project";
|
|
35
|
+
let language = "unknown";
|
|
36
|
+
if (await has("next.config.js") || await has("next.config.ts") || await has("next.config.mjs")) {
|
|
37
|
+
framework = `Next.js ${deps.next?.replace(/[^\d.]/g, "") ?? ""}`.trim();
|
|
38
|
+
language = await has("tsconfig.json") ? "TypeScript" : "JavaScript";
|
|
39
|
+
} else if (await has("svelte.config.js") || await has("svelte.config.ts")) {
|
|
40
|
+
framework = "SvelteKit";
|
|
41
|
+
language = "TypeScript";
|
|
42
|
+
} else if (await has("astro.config.mjs") || await has("astro.config.ts")) {
|
|
43
|
+
framework = "Astro";
|
|
44
|
+
language = "TypeScript";
|
|
45
|
+
} else if (await has("nuxt.config.ts")) {
|
|
46
|
+
framework = "Nuxt";
|
|
47
|
+
language = "TypeScript";
|
|
48
|
+
} else if (await has("Gemfile")) {
|
|
49
|
+
framework = "Rails";
|
|
50
|
+
language = "Ruby";
|
|
51
|
+
} else if (await has("manage.py")) {
|
|
52
|
+
framework = "Django";
|
|
53
|
+
language = "Python";
|
|
54
|
+
} else if (await has("pyproject.toml")) {
|
|
55
|
+
framework = "Python project";
|
|
56
|
+
language = "Python";
|
|
57
|
+
} else if (await has("Cargo.toml")) {
|
|
58
|
+
framework = "Rust project";
|
|
59
|
+
language = "Rust";
|
|
60
|
+
} else if (await has("package.json")) {
|
|
61
|
+
framework = "Node.js";
|
|
62
|
+
language = await has("tsconfig.json") ? "TypeScript" : "JavaScript";
|
|
63
|
+
}
|
|
64
|
+
let db = "none / not detected";
|
|
65
|
+
let dbTypesPath = "src/types/database.ts";
|
|
66
|
+
if (await has("supabase") || deps["@supabase/supabase-js"]) {
|
|
67
|
+
db = "Supabase (Postgres + Auth + Realtime + Storage)";
|
|
68
|
+
dbTypesPath = "src/types/database.ts";
|
|
69
|
+
} else if (await has("prisma/schema.prisma")) {
|
|
70
|
+
db = "Prisma + Postgres";
|
|
71
|
+
dbTypesPath = "prisma/schema.prisma";
|
|
72
|
+
} else if (await has("drizzle.config.ts") || await has("drizzle.config.js")) {
|
|
73
|
+
db = "Drizzle ORM";
|
|
74
|
+
dbTypesPath = "src/db/schema.ts";
|
|
75
|
+
} else if (deps.typeorm) {
|
|
76
|
+
db = "TypeORM";
|
|
77
|
+
dbTypesPath = "src/entities/";
|
|
78
|
+
} else if (deps.sequelize) {
|
|
79
|
+
db = "Sequelize";
|
|
80
|
+
dbTypesPath = "src/models/";
|
|
81
|
+
}
|
|
82
|
+
let ui = "plain CSS";
|
|
83
|
+
if (deps.tailwindcss) ui = "Tailwind CSS";
|
|
84
|
+
if (deps["@radix-ui/react-dialog"] || deps["shadcn-ui"]) ui = "Tailwind + shadcn/ui";
|
|
85
|
+
if (deps["@mui/material"]) ui = "MUI";
|
|
86
|
+
if (deps["@chakra-ui/react"]) ui = "Chakra UI";
|
|
87
|
+
let payment = "none";
|
|
88
|
+
if (deps.stripe || deps["@stripe/stripe-js"]) payment = "Stripe";
|
|
89
|
+
if (deps["@tosspayments/payment-sdk"]) payment = "Toss Payments";
|
|
90
|
+
const scripts = pkg?.scripts ?? {};
|
|
91
|
+
const buildCmd = scripts.build ? "npm run build" : "npm run build";
|
|
92
|
+
const typecheckCmd = scripts.typecheck ? "npm run typecheck" : language === "TypeScript" ? "npx tsc --noEmit" : "";
|
|
93
|
+
const testCmd = scripts.test ? "npm test" : "";
|
|
94
|
+
const e2eCmd = scripts["test:e2e"] ? "npm run test:e2e" : "";
|
|
95
|
+
let ghUser = "your-github-handle";
|
|
96
|
+
try {
|
|
97
|
+
const out = execSync("git config user.name", { cwd: target, encoding: "utf8" }).trim();
|
|
98
|
+
if (out) ghUser = out;
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
let routeGroups = "see project";
|
|
102
|
+
let middlewarePath = "middleware.ts";
|
|
103
|
+
if (framework.startsWith("Next.js")) {
|
|
104
|
+
const groups = [];
|
|
105
|
+
if (await has("src/app/(public)")) groups.push("(public)");
|
|
106
|
+
if (await has("src/app/(auth)")) groups.push("(auth)");
|
|
107
|
+
if (await has("src/app/(dashboard)")) groups.push("(dashboard)");
|
|
108
|
+
if (await has("src/app/(admin)")) groups.push("(admin)");
|
|
109
|
+
routeGroups = groups.length > 0 ? groups.join(" / ") : "src/app/";
|
|
110
|
+
if (await has("src/proxy.ts")) middlewarePath = "src/proxy.ts";
|
|
111
|
+
else if (await has("middleware.ts")) middlewarePath = "middleware.ts";
|
|
112
|
+
else if (await has("src/middleware.ts")) middlewarePath = "src/middleware.ts";
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
framework,
|
|
116
|
+
language,
|
|
117
|
+
db,
|
|
118
|
+
ui,
|
|
119
|
+
payment,
|
|
120
|
+
ghUser,
|
|
121
|
+
buildCmd,
|
|
122
|
+
typecheckCmd,
|
|
123
|
+
testCmd,
|
|
124
|
+
e2eCmd,
|
|
125
|
+
dbTypesPath,
|
|
126
|
+
routeGroups,
|
|
127
|
+
middlewarePath
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/template.ts
|
|
132
|
+
function renderTemplate(content, context) {
|
|
133
|
+
return content.replace(/\{\{\s*([A-Z_][A-Z0-9_]*)\s*\}\}/g, (match, key) => {
|
|
134
|
+
if (key in context && context[key] !== void 0 && context[key] !== "") {
|
|
135
|
+
return context[key];
|
|
136
|
+
}
|
|
137
|
+
return match;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function countUnfilled(rendered) {
|
|
141
|
+
return (rendered.match(/\{\{\s*[A-Z_][A-Z0-9_]*\s*\}\}/g) ?? []).length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/init.ts
|
|
145
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
146
|
+
var __dirname2 = path2.dirname(__filename2);
|
|
147
|
+
var c = {
|
|
148
|
+
bold: (s) => `\x1B[1m${s}\x1B[22m`,
|
|
149
|
+
dim: (s) => `\x1B[2m${s}\x1B[22m`,
|
|
150
|
+
green: (s) => `\x1B[32m${s}\x1B[39m`,
|
|
151
|
+
red: (s) => `\x1B[31m${s}\x1B[39m`,
|
|
152
|
+
yellow: (s) => `\x1B[33m${s}\x1B[39m`,
|
|
153
|
+
cyan: (s) => `\x1B[36m${s}\x1B[39m`
|
|
154
|
+
};
|
|
155
|
+
async function resolveAssetPaths() {
|
|
156
|
+
const packaged = path2.resolve(__dirname2, "..", "templates");
|
|
157
|
+
if (await exists2(packaged)) {
|
|
158
|
+
return {
|
|
159
|
+
agents: path2.join(packaged, "agents"),
|
|
160
|
+
templates: path2.join(packaged, "templates"),
|
|
161
|
+
projectTemplate: path2.join(packaged, "project-template"),
|
|
162
|
+
mode: "packaged"
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const monorepoRoot = path2.resolve(__dirname2, "../../..");
|
|
166
|
+
const sharedDir = path2.join(monorepoRoot, "shared");
|
|
167
|
+
if (await exists2(sharedDir)) {
|
|
168
|
+
return {
|
|
169
|
+
agents: path2.join(sharedDir, "agents"),
|
|
170
|
+
templates: path2.join(sharedDir, "templates"),
|
|
171
|
+
projectTemplate: path2.join(monorepoRoot, "packages/template"),
|
|
172
|
+
mode: "monorepo"
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Could not locate harness-bujang assets. Tried:
|
|
177
|
+
- ${packaged}
|
|
178
|
+
- ${sharedDir}
|
|
179
|
+
If installed via npm, try reinstalling. If running from source, run "npm run build" in packages/cli first.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
async function runInit(args) {
|
|
183
|
+
const opts = parseArgs(args);
|
|
184
|
+
const assets = await resolveAssetPaths();
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(c.bold("\u{1F4E6} Harness-Bujang init"));
|
|
187
|
+
console.log(c.dim(` Target: ${opts.target}`));
|
|
188
|
+
console.log(c.dim(` Language: ${opts.lang}`));
|
|
189
|
+
console.log(c.dim(` Chat backend: ${opts.chatBackend}${opts.chatBackend === "sqlite" ? c.dim(" (default \u2014 local file)") : c.dim(" (cloud Postgres)")}`));
|
|
190
|
+
console.log(c.dim(` Assets: ${assets.mode}`));
|
|
191
|
+
console.log();
|
|
192
|
+
if (!await exists2(opts.target)) {
|
|
193
|
+
throw new Error(`Target directory does not exist: ${opts.target}`);
|
|
194
|
+
}
|
|
195
|
+
const scan = await scanProject(opts.target);
|
|
196
|
+
console.log(c.bold("\u{1F50D} Detected"));
|
|
197
|
+
console.log(` Framework: ${scan.framework}`);
|
|
198
|
+
console.log(` Language: ${scan.language}`);
|
|
199
|
+
console.log(` DB: ${scan.db}`);
|
|
200
|
+
console.log(` UI: ${scan.ui}`);
|
|
201
|
+
console.log(` Payment: ${scan.payment}`);
|
|
202
|
+
console.log(` GitHub: ${scan.ghUser}`);
|
|
203
|
+
console.log();
|
|
204
|
+
const context = {
|
|
205
|
+
PROJECT_PATH: opts.target,
|
|
206
|
+
PROJECT_NAME: path2.basename(opts.target),
|
|
207
|
+
PROJECT_CATEGORY: scan.framework.startsWith("Next.js") ? "Web application" : "Software project",
|
|
208
|
+
DIFFERENTIATION: "(define your project differentiation here if relevant)",
|
|
209
|
+
STACK_FRAMEWORK: opts.framework ?? scan.framework,
|
|
210
|
+
STACK_LANGUAGE: scan.language,
|
|
211
|
+
STACK_DB: opts.db ?? scan.db,
|
|
212
|
+
STACK_UI: scan.ui,
|
|
213
|
+
STACK_PAYMENT: scan.payment,
|
|
214
|
+
STACK_EXTRA: "(none)",
|
|
215
|
+
HARNESS_TABLE: "harness_messages",
|
|
216
|
+
ADMIN_HARNESS_ROUTE: "/admin/harness",
|
|
217
|
+
LEARNING_LOG_PATH: "docs/AGENT_LEARNING_LOG.md",
|
|
218
|
+
TASKS_TRACKER_GLOB: "docs/TASKS_*.md",
|
|
219
|
+
BENCHMARK_DOC_PATH: "docs/BENCHMARK.md",
|
|
220
|
+
GH_USER: scan.ghUser,
|
|
221
|
+
BUILD_CMD: scan.buildCmd || "(no build script \u2014 add one if applicable)",
|
|
222
|
+
TYPECHECK_CMD: scan.typecheckCmd || "(no type-check command \u2014 language may not be statically typed)",
|
|
223
|
+
TEST_CMD: scan.testCmd || "(no tests configured)",
|
|
224
|
+
E2E_CMD: scan.e2eCmd || "(no E2E setup)",
|
|
225
|
+
DEV_URL: "http://localhost:3000",
|
|
226
|
+
DB_TYPES_PATH: scan.dbTypesPath,
|
|
227
|
+
DB_CLIENT_PATTERN: `Use the project's existing DB client convention. See ${scan.dbTypesPath} for types.`,
|
|
228
|
+
KNOWN_SCHEMA_DRIFT: "(none documented yet)",
|
|
229
|
+
COMMON_FK_HINTS: "(extract from your schema as you go)",
|
|
230
|
+
ACCESS_POLICY_NOTES: "(document RLS / middleware / controller guards as you encounter them)",
|
|
231
|
+
MIGRATION_NAMING: "supabase/migrations/XXXXX_name.sql (or per-stack)",
|
|
232
|
+
MIGRATION_APPLY_CMD: "supabase db push (or stack-specific)",
|
|
233
|
+
ROUTE_GROUPS: scan.routeGroups,
|
|
234
|
+
MIDDLEWARE_PATH: scan.middlewarePath,
|
|
235
|
+
KEY_RELATIONSHIPS: "(document key entity relations as you go)",
|
|
236
|
+
AUTH_GUARD_PATTERN: "(stack-specific \u2014 e.g. supabase.auth.getUser())",
|
|
237
|
+
ADMIN_GUARD_PATTERN: "(stack-specific \u2014 e.g. verifyAdmin())",
|
|
238
|
+
API_RESPONSE_SHAPE: "{ data, error, message }",
|
|
239
|
+
PRIMARY_COLOR: "#6366F1",
|
|
240
|
+
FRAMEWORK_REVIEW_RULES: stackReviewRules(scan.framework),
|
|
241
|
+
TEST_ACCOUNTS: "(define your test accounts here)",
|
|
242
|
+
LEGAL_CONTEXT: '(no special legal context \u2014 remove "Legal/terms" rows in director.md if not applicable)',
|
|
243
|
+
LANG_CODE: opts.lang,
|
|
244
|
+
TODAY: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
245
|
+
COMPLETED_DOCS_PATTERN: "docs/\uC644\uB8CC_*.md"
|
|
246
|
+
};
|
|
247
|
+
console.log(c.bold(`\u{1F4C2} Installing agents to .claude/agents/`));
|
|
248
|
+
const agentsSrc = path2.join(assets.agents, opts.lang);
|
|
249
|
+
const agentsDst = path2.join(opts.target, ".claude/agents");
|
|
250
|
+
await fs2.mkdir(agentsDst, { recursive: true });
|
|
251
|
+
const agentFiles = (await fs2.readdir(agentsSrc)).filter((f) => f.endsWith(".md"));
|
|
252
|
+
for (const f of agentFiles) {
|
|
253
|
+
const dst = path2.join(agentsDst, f);
|
|
254
|
+
if (await exists2(dst) && !opts.yes) {
|
|
255
|
+
console.log(` ${c.yellow("\u26A0")} ${f} ${c.dim("(exists, skipped \u2014 use --yes to overwrite)")}`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const raw = await fs2.readFile(path2.join(agentsSrc, f), "utf8");
|
|
259
|
+
await fs2.writeFile(dst, renderTemplate(raw, context));
|
|
260
|
+
console.log(` ${c.green("\u2713")} ${f}`);
|
|
261
|
+
}
|
|
262
|
+
console.log();
|
|
263
|
+
if (opts.editClaudeMd) {
|
|
264
|
+
console.log(c.bold("\u{1F4DD} Updating CLAUDE.md"));
|
|
265
|
+
const sectionTpl = await fs2.readFile(
|
|
266
|
+
path2.join(assets.templates, opts.lang, "CLAUDE.md.harness-section.template"),
|
|
267
|
+
"utf8"
|
|
268
|
+
);
|
|
269
|
+
const section = renderTemplate(sectionTpl, context);
|
|
270
|
+
const claudeMdPath = path2.join(opts.target, "CLAUDE.md");
|
|
271
|
+
if (await exists2(claudeMdPath)) {
|
|
272
|
+
const existing = await fs2.readFile(claudeMdPath, "utf8");
|
|
273
|
+
const alreadyHas = existing.includes("\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1") || existing.includes("Harness Engineering");
|
|
274
|
+
if (alreadyHas) {
|
|
275
|
+
console.log(` ${c.yellow("\u26A0")} Section already present \u2014 skipped`);
|
|
276
|
+
} else {
|
|
277
|
+
await fs2.writeFile(claudeMdPath, existing.trimEnd() + "\n\n" + section + "\n");
|
|
278
|
+
console.log(` ${c.green("\u2713")} Appended harness section`);
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
await fs2.writeFile(claudeMdPath, section + "\n");
|
|
282
|
+
console.log(` ${c.green("\u2713")} Created new CLAUDE.md with harness section`);
|
|
283
|
+
}
|
|
284
|
+
console.log();
|
|
285
|
+
}
|
|
286
|
+
if (opts.seedLearningLog) {
|
|
287
|
+
console.log(c.bold("\u{1F9E0} Seeding learning log"));
|
|
288
|
+
const seedPath = path2.join(assets.templates, opts.lang, "AGENT_LEARNING_LOG.seed.md");
|
|
289
|
+
const seedRaw = await fs2.readFile(seedPath, "utf8");
|
|
290
|
+
const targetLog = path2.join(opts.target, context.LEARNING_LOG_PATH);
|
|
291
|
+
if (await exists2(targetLog)) {
|
|
292
|
+
console.log(` ${c.yellow("\u26A0")} ${context.LEARNING_LOG_PATH} already exists \u2014 skipped`);
|
|
293
|
+
} else {
|
|
294
|
+
await fs2.mkdir(path2.dirname(targetLog), { recursive: true });
|
|
295
|
+
await fs2.writeFile(targetLog, renderTemplate(seedRaw, context));
|
|
296
|
+
console.log(` ${c.green("\u2713")} ${context.LEARNING_LOG_PATH}`);
|
|
297
|
+
}
|
|
298
|
+
console.log();
|
|
299
|
+
}
|
|
300
|
+
if (opts.installTemplate) {
|
|
301
|
+
if (scan.framework.startsWith("Next.js")) {
|
|
302
|
+
console.log(c.bold("\u{1F4AC} Installing chat-room UI"));
|
|
303
|
+
await copyDir(
|
|
304
|
+
path2.join(assets.projectTemplate, "app/admin/harness"),
|
|
305
|
+
path2.join(opts.target, "src/app/admin/harness"),
|
|
306
|
+
opts.yes,
|
|
307
|
+
" "
|
|
308
|
+
);
|
|
309
|
+
await copyDir(
|
|
310
|
+
path2.join(assets.projectTemplate, "app/api/harness"),
|
|
311
|
+
path2.join(opts.target, "src/app/api/harness"),
|
|
312
|
+
opts.yes,
|
|
313
|
+
" "
|
|
314
|
+
);
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(c.bold("\u{1F5C4}\uFE0F Installing DB adapter library"));
|
|
317
|
+
await copyDir(
|
|
318
|
+
path2.join(assets.projectTemplate, "lib/harness-db"),
|
|
319
|
+
path2.join(opts.target, "src/lib/harness-db"),
|
|
320
|
+
opts.yes,
|
|
321
|
+
" "
|
|
322
|
+
);
|
|
323
|
+
console.log();
|
|
324
|
+
if (opts.chatBackend === "supabase") {
|
|
325
|
+
console.log(c.bold("\u{1F5C4}\uFE0F Copying Supabase migrations"));
|
|
326
|
+
await copyDir(
|
|
327
|
+
path2.join(assets.projectTemplate, "migrations"),
|
|
328
|
+
path2.join(opts.target, "supabase/migrations"),
|
|
329
|
+
opts.yes,
|
|
330
|
+
" ",
|
|
331
|
+
/^00010_|^00025_/
|
|
332
|
+
);
|
|
333
|
+
console.log();
|
|
334
|
+
}
|
|
335
|
+
if (opts.chatBackend === "sqlite" && !opts.commitChat) {
|
|
336
|
+
await ensureGitignore(opts.target, [".harness/"]);
|
|
337
|
+
}
|
|
338
|
+
printBackendInstructions(opts.chatBackend, opts.commitChat);
|
|
339
|
+
} else {
|
|
340
|
+
console.log(
|
|
341
|
+
`${c.yellow("\u26A0 Chat-room UI is Next.js only.")} ` + c.dim(`Skipping \u2014 your stack is detected as ${scan.framework}.`)
|
|
342
|
+
);
|
|
343
|
+
console.log(
|
|
344
|
+
c.dim(" (For non-Next.js stacks, use the agents only \u2014 chat-room support is on the roadmap.)")
|
|
345
|
+
);
|
|
346
|
+
console.log();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
console.log(c.bold(c.green("\u2705 Done.")));
|
|
350
|
+
console.log();
|
|
351
|
+
console.log("Next steps:");
|
|
352
|
+
console.log(` ${c.cyan("1.")} Open Claude Code in this project`);
|
|
353
|
+
console.log(` ${c.cyan("2.")} Run ${c.bold("/bujang-status")} (if the plugin is installed) or just`);
|
|
354
|
+
console.log(` ask ${c.bold('"Director, please add a hello-world endpoint"')}`);
|
|
355
|
+
console.log(` ${c.cyan("3.")} Watch ${c.bold(context.ADMIN_HARNESS_ROUTE)} for live updates (after env setup)`);
|
|
356
|
+
console.log();
|
|
357
|
+
}
|
|
358
|
+
function parseArgs(args) {
|
|
359
|
+
const lang = getFlag(args, "--lang") ?? "en";
|
|
360
|
+
if (!["ko", "en"].includes(lang)) {
|
|
361
|
+
throw new Error(`--lang must be "ko" or "en", got "${lang}"`);
|
|
362
|
+
}
|
|
363
|
+
const chatBackend = getFlag(args, "--chat") ?? "sqlite";
|
|
364
|
+
if (!["sqlite", "supabase"].includes(chatBackend)) {
|
|
365
|
+
throw new Error(`--chat must be "sqlite" or "supabase", got "${chatBackend}"`);
|
|
366
|
+
}
|
|
367
|
+
const targetRaw = getFlag(args, "--target") ?? ".";
|
|
368
|
+
return {
|
|
369
|
+
lang,
|
|
370
|
+
target: path2.resolve(targetRaw),
|
|
371
|
+
framework: getFlag(args, "--framework"),
|
|
372
|
+
db: getFlag(args, "--db"),
|
|
373
|
+
chatBackend,
|
|
374
|
+
commitChat: args.includes("--commit-chat"),
|
|
375
|
+
installTemplate: !args.includes("--no-template"),
|
|
376
|
+
editClaudeMd: !args.includes("--no-claude-md"),
|
|
377
|
+
seedLearningLog: !args.includes("--no-learning-log"),
|
|
378
|
+
yes: args.includes("--yes") || args.includes("-y")
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function getFlag(args, name) {
|
|
382
|
+
for (const a of args) {
|
|
383
|
+
if (a.startsWith(`${name}=`)) return a.slice(name.length + 1);
|
|
384
|
+
}
|
|
385
|
+
const idx = args.indexOf(name);
|
|
386
|
+
if (idx >= 0 && idx + 1 < args.length && !args[idx + 1].startsWith("--")) {
|
|
387
|
+
return args[idx + 1];
|
|
388
|
+
}
|
|
389
|
+
return void 0;
|
|
390
|
+
}
|
|
391
|
+
async function exists2(p) {
|
|
392
|
+
try {
|
|
393
|
+
await fs2.access(p);
|
|
394
|
+
return true;
|
|
395
|
+
} catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function copyDir(src, dst, overwrite, indent, filter) {
|
|
400
|
+
await fs2.mkdir(dst, { recursive: true });
|
|
401
|
+
const entries = await fs2.readdir(src, { withFileTypes: true });
|
|
402
|
+
for (const e of entries) {
|
|
403
|
+
if (filter && !e.isDirectory() && !filter.test(e.name)) continue;
|
|
404
|
+
const s = path2.join(src, e.name);
|
|
405
|
+
const d = path2.join(dst, e.name);
|
|
406
|
+
if (e.isDirectory()) {
|
|
407
|
+
await copyDir(s, d, overwrite, indent, filter);
|
|
408
|
+
} else {
|
|
409
|
+
if (await exists2(d) && !overwrite) {
|
|
410
|
+
console.log(`${indent}${c.yellow("\u26A0")} ${path2.relative(process.cwd(), d)} ${c.dim("(exists, skipped)")}`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
await fs2.copyFile(s, d);
|
|
414
|
+
console.log(`${indent}${c.green("\u2713")} ${path2.relative(process.cwd(), d)}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function ensureGitignore(target, lines) {
|
|
419
|
+
const gitignorePath = path2.join(target, ".gitignore");
|
|
420
|
+
let existing = "";
|
|
421
|
+
if (await exists2(gitignorePath)) {
|
|
422
|
+
existing = await fs2.readFile(gitignorePath, "utf8");
|
|
423
|
+
}
|
|
424
|
+
const toAdd = lines.filter((l) => !existing.split("\n").some((e) => e.trim() === l.trim()));
|
|
425
|
+
if (toAdd.length === 0) return;
|
|
426
|
+
const sep = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
427
|
+
const block = `${sep}
|
|
428
|
+
# Harness-Bujang local chat database
|
|
429
|
+
${toAdd.join("\n")}
|
|
430
|
+
`;
|
|
431
|
+
await fs2.writeFile(gitignorePath, existing + block);
|
|
432
|
+
}
|
|
433
|
+
function printBackendInstructions(backend, commitChat) {
|
|
434
|
+
if (backend === "sqlite") {
|
|
435
|
+
console.log(c.bold(c.cyan("\u{1F4CB} SQLite mode (default) \u2014 next steps:")));
|
|
436
|
+
console.log();
|
|
437
|
+
console.log(` ${c.bold("1.")} Install the SQLite driver in your project:`);
|
|
438
|
+
console.log(` ${c.dim("$")} ${c.bold("npm i better-sqlite3")}`);
|
|
439
|
+
console.log(` ${c.dim("$")} ${c.bold("npm i -D @types/better-sqlite3")}`);
|
|
440
|
+
console.log();
|
|
441
|
+
console.log(` ${c.bold("2.")} (optional) Add to ${c.bold(".env.local")}:`);
|
|
442
|
+
console.log(` ${c.dim("HARNESS_DB=sqlite # default \u2014 can be omitted")}`);
|
|
443
|
+
console.log(` ${c.dim("HARNESS_SQLITE_PATH=./.harness/chat.db # default \u2014 can be omitted")}`);
|
|
444
|
+
console.log(` ${c.bold("HARNESS_WRITE_SECRET=<random> # for bot/script writes")}`);
|
|
445
|
+
console.log(` ${c.bold("SUPER_ADMIN_EMAILS=you@example.com # comma-separated")}`);
|
|
446
|
+
console.log();
|
|
447
|
+
console.log(` ${c.bold("3.")} Run your dev server and visit ${c.bold("/admin/harness")}.`);
|
|
448
|
+
console.log();
|
|
449
|
+
if (commitChat) {
|
|
450
|
+
console.log(c.dim(` \u{1F4E6} ${c.bold("--commit-chat")} mode: .harness/ NOT added to .gitignore.`));
|
|
451
|
+
console.log(c.dim(` Commit chat.db to sync history across YOUR OWN machines.`));
|
|
452
|
+
console.log(c.dim(` \u26A0 Do NOT use this with multiple collaborators \u2014 binary files don't merge.`));
|
|
453
|
+
console.log(c.dim(` For team sharing, use --chat=supabase or "bujang migrate --to=supabase".`));
|
|
454
|
+
} else {
|
|
455
|
+
console.log(c.dim(` When you're ready for team / prod sharing, run:`));
|
|
456
|
+
console.log(c.dim(` $ bujang migrate --to=supabase`));
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
console.log(c.bold(c.cyan("\u{1F4CB} Supabase mode \u2014 next steps:")));
|
|
460
|
+
console.log();
|
|
461
|
+
console.log(` ${c.bold("1.")} Apply the migrations to your Supabase project:`);
|
|
462
|
+
console.log(` ${c.dim("$")} ${c.bold("supabase db push")}`);
|
|
463
|
+
console.log(` ${c.dim(" (or run them manually via psql / SQL editor)")}`);
|
|
464
|
+
console.log();
|
|
465
|
+
console.log(` ${c.bold("2.")} Add to ${c.bold(".env.local")}:`);
|
|
466
|
+
console.log(` ${c.bold("HARNESS_DB=supabase")}`);
|
|
467
|
+
console.log(` ${c.bold("NEXT_PUBLIC_SUPABASE_URL=...")}`);
|
|
468
|
+
console.log(` ${c.bold("SUPABASE_SERVICE_ROLE_KEY=...")}`);
|
|
469
|
+
console.log(` ${c.bold("HARNESS_WRITE_SECRET=<random>")}`);
|
|
470
|
+
console.log(` ${c.bold("SUPER_ADMIN_EMAILS=you@example.com")}`);
|
|
471
|
+
console.log();
|
|
472
|
+
console.log(` ${c.bold("3.")} Implement ${c.bold("verifySuperAdmin()")} at ${c.bold("@/lib/utils/admin")}`);
|
|
473
|
+
console.log(` (see ${c.dim("packages/template/README.md")} for an example).`);
|
|
474
|
+
console.log();
|
|
475
|
+
console.log(` ${c.bold("4.")} Run your dev server and visit ${c.bold("/admin/harness")}.`);
|
|
476
|
+
}
|
|
477
|
+
console.log();
|
|
478
|
+
}
|
|
479
|
+
function stackReviewRules(framework) {
|
|
480
|
+
if (framework.startsWith("Next.js")) {
|
|
481
|
+
return `Next.js App Router rules:
|
|
482
|
+
- Avoid unnecessary 'use client' (prefer Server Components)
|
|
483
|
+
- Radix UI hydration: Sheet/Dialog need a 'mounted' guard
|
|
484
|
+
- Hook dependency arrays must be exact
|
|
485
|
+
- Dynamic params: \`Promise<{ id: string }>\` + await`;
|
|
486
|
+
}
|
|
487
|
+
if (framework === "SvelteKit" || framework === "Astro" || framework === "Nuxt") {
|
|
488
|
+
return `${framework} rules: prefer SSR by default, use client islands only when interactive.`;
|
|
489
|
+
}
|
|
490
|
+
return "Project conventions: see root CLAUDE.md.";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/status.ts
|
|
494
|
+
import * as fs3 from "fs/promises";
|
|
495
|
+
import * as path3 from "path";
|
|
496
|
+
var c2 = {
|
|
497
|
+
bold: (s) => `\x1B[1m${s}\x1B[22m`,
|
|
498
|
+
dim: (s) => `\x1B[2m${s}\x1B[22m`,
|
|
499
|
+
green: (s) => `\x1B[32m${s}\x1B[39m`,
|
|
500
|
+
red: (s) => `\x1B[31m${s}\x1B[39m`,
|
|
501
|
+
yellow: (s) => `\x1B[33m${s}\x1B[39m`
|
|
502
|
+
};
|
|
503
|
+
var CANONICAL_AGENTS = [
|
|
504
|
+
"director",
|
|
505
|
+
"consultant",
|
|
506
|
+
"dev-team",
|
|
507
|
+
"architect-team",
|
|
508
|
+
"doc-sync-team",
|
|
509
|
+
"code-review-team",
|
|
510
|
+
"security-team",
|
|
511
|
+
"db-guard-team",
|
|
512
|
+
"qa-team",
|
|
513
|
+
"verifier-team"
|
|
514
|
+
];
|
|
515
|
+
async function runStatus(args) {
|
|
516
|
+
const target = path3.resolve(args.find((a) => !a.startsWith("--")) ?? ".");
|
|
517
|
+
console.log();
|
|
518
|
+
console.log(c2.bold(`\u{1F4CB} Harness-Bujang status \u2014 ${path3.basename(target)}`));
|
|
519
|
+
console.log(c2.dim(` ${target}`));
|
|
520
|
+
console.log();
|
|
521
|
+
let healthy = 0;
|
|
522
|
+
let total = 0;
|
|
523
|
+
console.log(c2.bold("Agents"));
|
|
524
|
+
const agentsDir = path3.join(target, ".claude/agents");
|
|
525
|
+
const agentsExists = await exists3(agentsDir);
|
|
526
|
+
if (!agentsExists) {
|
|
527
|
+
console.log(` ${c2.red("\u2716")} .claude/agents/ not found`);
|
|
528
|
+
total++;
|
|
529
|
+
} else {
|
|
530
|
+
const found = (await fs3.readdir(agentsDir)).filter((f) => f.endsWith(".md"));
|
|
531
|
+
for (const name of CANONICAL_AGENTS) {
|
|
532
|
+
total++;
|
|
533
|
+
const file = `${name}.md`;
|
|
534
|
+
if (!found.includes(file)) {
|
|
535
|
+
console.log(` ${c2.red("\u2716")} ${file} ${c2.dim("(missing)")}`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const raw = await fs3.readFile(path3.join(agentsDir, file), "utf8");
|
|
539
|
+
const unfilled = countUnfilled(raw);
|
|
540
|
+
if (unfilled > 0) {
|
|
541
|
+
console.log(` ${c2.yellow("\u26A0")} ${file} ${c2.dim(`(${unfilled} unfilled placeholders)`)}`);
|
|
542
|
+
} else {
|
|
543
|
+
console.log(` ${c2.green("\u2713")} ${file}`);
|
|
544
|
+
healthy++;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
console.log();
|
|
549
|
+
console.log(c2.bold("CLAUDE.md"));
|
|
550
|
+
const claudeMd = path3.join(target, "CLAUDE.md");
|
|
551
|
+
total++;
|
|
552
|
+
if (await exists3(claudeMd)) {
|
|
553
|
+
const text = await fs3.readFile(claudeMd, "utf8");
|
|
554
|
+
const hasSection = text.includes("\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1") || text.includes("Harness Engineering");
|
|
555
|
+
if (!hasSection) {
|
|
556
|
+
console.log(` ${c2.red("\u2716")} No harness section`);
|
|
557
|
+
} else {
|
|
558
|
+
const unfilled = countUnfilled(text);
|
|
559
|
+
if (unfilled > 0) {
|
|
560
|
+
console.log(` ${c2.yellow("\u26A0")} ${unfilled} unfilled placeholders`);
|
|
561
|
+
} else {
|
|
562
|
+
console.log(` ${c2.green("\u2713")} Section present, no unfilled placeholders`);
|
|
563
|
+
healthy++;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
console.log(` ${c2.red("\u2716")} CLAUDE.md not found`);
|
|
568
|
+
}
|
|
569
|
+
console.log();
|
|
570
|
+
console.log(c2.bold("Learning log"));
|
|
571
|
+
total++;
|
|
572
|
+
const candidates = [
|
|
573
|
+
"docs/AGENT_LEARNING_LOG.md",
|
|
574
|
+
"docs/\uAE30\uC874/AGENT_LEARNING_LOG.md",
|
|
575
|
+
"AGENT_LEARNING_LOG.md"
|
|
576
|
+
];
|
|
577
|
+
let foundLog = null;
|
|
578
|
+
for (const cand of candidates) {
|
|
579
|
+
if (await exists3(path3.join(target, cand))) {
|
|
580
|
+
foundLog = cand;
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (foundLog) {
|
|
585
|
+
console.log(` ${c2.green("\u2713")} ${foundLog}`);
|
|
586
|
+
healthy++;
|
|
587
|
+
} else {
|
|
588
|
+
console.log(` ${c2.yellow("\u26A0")} not found in standard locations`);
|
|
589
|
+
}
|
|
590
|
+
console.log();
|
|
591
|
+
console.log(c2.bold("Chat-room UI (optional)"));
|
|
592
|
+
const uiPath = path3.join(target, "src/app/admin/harness/page.tsx");
|
|
593
|
+
const apiLogsPath = path3.join(target, "src/app/api/harness/logs/route.ts");
|
|
594
|
+
if (await exists3(uiPath) && await exists3(apiLogsPath)) {
|
|
595
|
+
console.log(` ${c2.green("\u2713")} page.tsx + api routes installed`);
|
|
596
|
+
} else {
|
|
597
|
+
console.log(` ${c2.dim("-")} not installed`);
|
|
598
|
+
}
|
|
599
|
+
console.log();
|
|
600
|
+
const ratio = total > 0 ? healthy / total : 0;
|
|
601
|
+
if (ratio >= 0.95) {
|
|
602
|
+
console.log(`Overall: ${c2.green(c2.bold("\u{1F7E2} healthy"))}`);
|
|
603
|
+
} else if (ratio >= 0.5) {
|
|
604
|
+
console.log(`Overall: ${c2.yellow(c2.bold("\u{1F7E1} partial"))} ${c2.dim(`\u2014 run "harness-bujang init" to complete`)}`);
|
|
605
|
+
} else {
|
|
606
|
+
console.log(`Overall: ${c2.red(c2.bold("\u{1F534} not installed"))} ${c2.dim(`\u2014 run "harness-bujang init"`)}`);
|
|
607
|
+
}
|
|
608
|
+
console.log();
|
|
609
|
+
}
|
|
610
|
+
async function exists3(p) {
|
|
611
|
+
try {
|
|
612
|
+
await fs3.access(p);
|
|
613
|
+
return true;
|
|
614
|
+
} catch {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/migrate.ts
|
|
620
|
+
import * as fs4 from "fs/promises";
|
|
621
|
+
import * as path4 from "path";
|
|
622
|
+
import { execSync as execSync2 } from "child_process";
|
|
623
|
+
var c3 = {
|
|
624
|
+
bold: (s) => `\x1B[1m${s}\x1B[22m`,
|
|
625
|
+
dim: (s) => `\x1B[2m${s}\x1B[22m`,
|
|
626
|
+
green: (s) => `\x1B[32m${s}\x1B[39m`,
|
|
627
|
+
red: (s) => `\x1B[31m${s}\x1B[39m`,
|
|
628
|
+
yellow: (s) => `\x1B[33m${s}\x1B[39m`,
|
|
629
|
+
cyan: (s) => `\x1B[36m${s}\x1B[39m`
|
|
630
|
+
};
|
|
631
|
+
async function runMigrate(args) {
|
|
632
|
+
const opts = parseArgs2(args);
|
|
633
|
+
console.log();
|
|
634
|
+
console.log(c3.bold(`\u{1F504} Harness-Bujang migrate \u2192 ${opts.to}`));
|
|
635
|
+
console.log();
|
|
636
|
+
const dbLibPath = path4.join(opts.target, "src/lib/harness-db");
|
|
637
|
+
if (!await exists4(dbLibPath)) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`lib/harness-db not found at ${dbLibPath}.
|
|
640
|
+
Run "npx harness-bujang init" first.`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
const envFile = path4.join(opts.target, ".env.local");
|
|
644
|
+
const currentBackend = await detectBackend(opts.target);
|
|
645
|
+
console.log(` ${c3.dim("Current backend:")} ${currentBackend ?? "(none \u2014 first install)"}`);
|
|
646
|
+
console.log(` ${c3.dim("Target backend: ")} ${opts.to}`);
|
|
647
|
+
console.log();
|
|
648
|
+
if (currentBackend === opts.to) {
|
|
649
|
+
console.log(c3.yellow(`Already on ${opts.to}. Nothing to do.`));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (!opts.yes) {
|
|
653
|
+
console.log(c3.yellow("\u26A0 This will:"));
|
|
654
|
+
if (opts.to === "supabase") {
|
|
655
|
+
console.log(" 1. Read all messages from .harness/chat.db");
|
|
656
|
+
console.log(" 2. Push them to your Supabase harness_messages table");
|
|
657
|
+
console.log(" 3. Update .env.local with HARNESS_DB=supabase");
|
|
658
|
+
console.log(" 4. Back up the SQLite file to .harness/chat.db.bak");
|
|
659
|
+
} else {
|
|
660
|
+
console.log(" 1. Pull all messages from your Supabase harness_messages table");
|
|
661
|
+
console.log(" 2. Write them into .harness/chat.db");
|
|
662
|
+
console.log(" 3. Update .env.local with HARNESS_DB=sqlite");
|
|
663
|
+
}
|
|
664
|
+
console.log();
|
|
665
|
+
console.log(c3.dim("Pass --yes to skip this confirmation."));
|
|
666
|
+
if (!await confirm("Continue?")) {
|
|
667
|
+
console.log(c3.dim("Aborted."));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (opts.to === "supabase") {
|
|
672
|
+
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
673
|
+
console.log(c3.red("\u2716 Missing Supabase env vars."));
|
|
674
|
+
console.log();
|
|
675
|
+
console.log(" Required (set in your shell or .env.local):");
|
|
676
|
+
console.log(` ${c3.bold("NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co")}`);
|
|
677
|
+
console.log(` ${c3.bold("SUPABASE_SERVICE_ROLE_KEY=eyJ...")}`);
|
|
678
|
+
console.log();
|
|
679
|
+
console.log(c3.dim(" Apply migrations first:"));
|
|
680
|
+
console.log(c3.dim(" $ supabase db push"));
|
|
681
|
+
console.log(c3.dim(" Or copy SQL from src/lib/harness-db/.. into your Supabase SQL editor."));
|
|
682
|
+
throw new Error("Supabase env not configured");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const transferScript = await writeTransferScript(opts.target, currentBackend ?? "sqlite", opts.to);
|
|
686
|
+
console.log(c3.bold("\u{1F69A} Transferring messages\u2026"));
|
|
687
|
+
try {
|
|
688
|
+
execSync2(`node "${transferScript}"`, {
|
|
689
|
+
cwd: opts.target,
|
|
690
|
+
stdio: "inherit",
|
|
691
|
+
env: { ...process.env, HARNESS_TRANSFER_FROM: currentBackend ?? "sqlite", HARNESS_TRANSFER_TO: opts.to }
|
|
692
|
+
});
|
|
693
|
+
} finally {
|
|
694
|
+
await fs4.unlink(transferScript).catch(() => void 0);
|
|
695
|
+
}
|
|
696
|
+
console.log();
|
|
697
|
+
console.log(c3.bold("\u{1F4DD} Updating .env.local"));
|
|
698
|
+
await upsertEnvVar(envFile, "HARNESS_DB", opts.to);
|
|
699
|
+
console.log(` ${c3.green("\u2713")} HARNESS_DB=${opts.to}`);
|
|
700
|
+
if (currentBackend === "sqlite" && opts.to === "supabase") {
|
|
701
|
+
const dbPath = path4.join(opts.target, ".harness/chat.db");
|
|
702
|
+
const bakPath = path4.join(opts.target, ".harness/chat.db.bak");
|
|
703
|
+
if (await exists4(dbPath)) {
|
|
704
|
+
await fs4.copyFile(dbPath, bakPath);
|
|
705
|
+
console.log(` ${c3.green("\u2713")} Backed up: .harness/chat.db.bak`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
console.log();
|
|
709
|
+
console.log(c3.bold(c3.green("\u2705 Migration done.")));
|
|
710
|
+
console.log();
|
|
711
|
+
console.log("Next steps:");
|
|
712
|
+
console.log(` 1. Restart your dev server (so HARNESS_DB takes effect)`);
|
|
713
|
+
console.log(` 2. Visit /admin/harness \u2014 messages should appear from the new backend`);
|
|
714
|
+
if (opts.to === "sqlite" && currentBackend === "supabase") {
|
|
715
|
+
console.log();
|
|
716
|
+
console.log(c3.dim(` (The Supabase table still has your data. Drop it manually if you want.)`));
|
|
717
|
+
}
|
|
718
|
+
console.log();
|
|
719
|
+
}
|
|
720
|
+
function parseArgs2(args) {
|
|
721
|
+
const to = getFlag2(args, "--to") ?? "";
|
|
722
|
+
if (!["sqlite", "supabase"].includes(to)) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
`--to is required and must be "sqlite" or "supabase".
|
|
725
|
+
Example: bujang migrate --to=supabase`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const targetRaw = getFlag2(args, "--target") ?? ".";
|
|
729
|
+
return {
|
|
730
|
+
to,
|
|
731
|
+
target: path4.resolve(targetRaw),
|
|
732
|
+
yes: args.includes("--yes") || args.includes("-y")
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function getFlag2(args, name) {
|
|
736
|
+
for (const a of args) {
|
|
737
|
+
if (a.startsWith(`${name}=`)) return a.slice(name.length + 1);
|
|
738
|
+
}
|
|
739
|
+
const idx = args.indexOf(name);
|
|
740
|
+
if (idx >= 0 && idx + 1 < args.length && !args[idx + 1].startsWith("--")) {
|
|
741
|
+
return args[idx + 1];
|
|
742
|
+
}
|
|
743
|
+
return void 0;
|
|
744
|
+
}
|
|
745
|
+
async function exists4(p) {
|
|
746
|
+
try {
|
|
747
|
+
await fs4.access(p);
|
|
748
|
+
return true;
|
|
749
|
+
} catch {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function detectBackend(target) {
|
|
754
|
+
for (const f of [".env.local", ".env"]) {
|
|
755
|
+
const p = path4.join(target, f);
|
|
756
|
+
if (!await exists4(p)) continue;
|
|
757
|
+
const text = await fs4.readFile(p, "utf8");
|
|
758
|
+
const m = text.match(/^HARNESS_DB\s*=\s*(\S+)/m);
|
|
759
|
+
if (m && (m[1] === "sqlite" || m[1] === "supabase")) return m[1];
|
|
760
|
+
}
|
|
761
|
+
if (await exists4(path4.join(target, ".harness/chat.db"))) return "sqlite";
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
async function upsertEnvVar(envFile, key, value) {
|
|
765
|
+
let content = "";
|
|
766
|
+
if (await exists4(envFile)) {
|
|
767
|
+
content = await fs4.readFile(envFile, "utf8");
|
|
768
|
+
}
|
|
769
|
+
const re = new RegExp(`^${key}\\s*=.*$`, "m");
|
|
770
|
+
if (re.test(content)) {
|
|
771
|
+
content = content.replace(re, `${key}=${value}`);
|
|
772
|
+
} else {
|
|
773
|
+
content += (content && !content.endsWith("\n") ? "\n" : "") + `${key}=${value}
|
|
774
|
+
`;
|
|
775
|
+
}
|
|
776
|
+
await fs4.writeFile(envFile, content);
|
|
777
|
+
}
|
|
778
|
+
async function confirm(message) {
|
|
779
|
+
process.stdout.write(`${message} [y/N] `);
|
|
780
|
+
return new Promise((resolve4) => {
|
|
781
|
+
process.stdin.setEncoding("utf8");
|
|
782
|
+
process.stdin.once("data", (chunk) => {
|
|
783
|
+
const ans = chunk.toString().trim().toLowerCase();
|
|
784
|
+
resolve4(ans === "y" || ans === "yes");
|
|
785
|
+
process.stdin.pause();
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async function writeTransferScript(target, from, to) {
|
|
790
|
+
const scriptPath = path4.join(target, ".harness-migrate-" + Date.now() + ".mjs");
|
|
791
|
+
const code = `
|
|
792
|
+
import { createAdapter } from './src/lib/harness-db/index.js';
|
|
793
|
+
|
|
794
|
+
const src = createAdapter('${from}');
|
|
795
|
+
const dst = createAdapter('${to}');
|
|
796
|
+
|
|
797
|
+
let total = 0;
|
|
798
|
+
let lastTs = undefined;
|
|
799
|
+
|
|
800
|
+
while (true) {
|
|
801
|
+
const batch = await src.list(lastTs ? { before: lastTs, limit: 200 } : { days: 36500 });
|
|
802
|
+
if (batch.length === 0) break;
|
|
803
|
+
for (const msg of batch) {
|
|
804
|
+
await dst.upsert(msg);
|
|
805
|
+
total++;
|
|
806
|
+
}
|
|
807
|
+
// Continue from the oldest seen so far to walk backwards through history.
|
|
808
|
+
lastTs = batch[0].timestamp;
|
|
809
|
+
if (!lastTs) break;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
console.log(' \u2713 Transferred ' + total + ' messages');
|
|
813
|
+
`;
|
|
814
|
+
await fs4.writeFile(scriptPath, code);
|
|
815
|
+
return scriptPath;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/index.ts
|
|
819
|
+
var c4 = {
|
|
820
|
+
bold: (s) => `\x1B[1m${s}\x1B[22m`,
|
|
821
|
+
dim: (s) => `\x1B[2m${s}\x1B[22m`,
|
|
822
|
+
green: (s) => `\x1B[32m${s}\x1B[39m`,
|
|
823
|
+
red: (s) => `\x1B[31m${s}\x1B[39m`,
|
|
824
|
+
yellow: (s) => `\x1B[33m${s}\x1B[39m`,
|
|
825
|
+
cyan: (s) => `\x1B[36m${s}\x1B[39m`
|
|
826
|
+
};
|
|
827
|
+
var HELP = `
|
|
828
|
+
${c4.bold("harness-bujang")} \u2014 Korean-style multi-agent harness director for Claude Code
|
|
829
|
+
${c4.dim("https://github.com/bjcho4141/harness-bujang")}
|
|
830
|
+
|
|
831
|
+
${c4.bold("Usage:")}
|
|
832
|
+
npx harness-bujang ${c4.cyan("init")} [options] Install the harness into a project
|
|
833
|
+
npx harness-bujang ${c4.cyan("status")} [options] Verify the harness install
|
|
834
|
+
npx harness-bujang ${c4.cyan("migrate")} --to=<sqlite|supabase> Move chat data between backends
|
|
835
|
+
|
|
836
|
+
${c4.bold("Options for init:")}
|
|
837
|
+
--lang=<ko|en> Agent language (default: en)
|
|
838
|
+
--chat=<sqlite|supabase> Chat-room backend (default: sqlite \u2014 local file, no setup)
|
|
839
|
+
--commit-chat Don't gitignore .harness/ (for solo cross-machine sync via git)
|
|
840
|
+
--target=<path> Project root (default: cwd)
|
|
841
|
+
--framework=<name> Override detected framework
|
|
842
|
+
--db=<name> Override detected project DB (separate from --chat)
|
|
843
|
+
--no-template Skip chat-room UI install
|
|
844
|
+
--no-claude-md Skip CLAUDE.md edit
|
|
845
|
+
--no-learning-log Skip learning log seed
|
|
846
|
+
--yes, -y Overwrite without asking
|
|
847
|
+
|
|
848
|
+
${c4.bold("Options for migrate:")}
|
|
849
|
+
--to=<sqlite|supabase> Required \u2014 target backend
|
|
850
|
+
--target=<path> Project root (default: cwd)
|
|
851
|
+
--yes, -y Skip confirmation
|
|
852
|
+
|
|
853
|
+
${c4.bold("Examples:")}
|
|
854
|
+
${c4.dim("# Install Korean Bujang persona, SQLite chat (default \u2014 zero setup)")}
|
|
855
|
+
npx harness-bujang init --lang=ko
|
|
856
|
+
|
|
857
|
+
${c4.dim("# Solo project: localhost-only, no cloud \u2014 done")}
|
|
858
|
+
npx harness-bujang init && npm run dev
|
|
859
|
+
${c4.dim("# Then: open localhost:3000/admin/harness")}
|
|
860
|
+
|
|
861
|
+
${c4.dim("# Solo, multiple machines \u2014 sync chat history via git")}
|
|
862
|
+
npx harness-bujang init --commit-chat
|
|
863
|
+
|
|
864
|
+
${c4.dim("# Production project with team sharing \u2014 Supabase backend")}
|
|
865
|
+
npx harness-bujang init --chat=supabase
|
|
866
|
+
|
|
867
|
+
${c4.dim("# Started solo, now scaling up \u2014 promote to cloud")}
|
|
868
|
+
bujang migrate --to=supabase
|
|
869
|
+
|
|
870
|
+
${c4.dim("# Going back to solo / archive \u2014 pull cloud data into local SQLite")}
|
|
871
|
+
bujang migrate --to=sqlite
|
|
872
|
+
`;
|
|
873
|
+
async function main() {
|
|
874
|
+
const args = process.argv.slice(2);
|
|
875
|
+
const command = args[0];
|
|
876
|
+
switch (command) {
|
|
877
|
+
case "init":
|
|
878
|
+
await runInit(args.slice(1));
|
|
879
|
+
break;
|
|
880
|
+
case "status":
|
|
881
|
+
await runStatus(args.slice(1));
|
|
882
|
+
break;
|
|
883
|
+
case "migrate":
|
|
884
|
+
await runMigrate(args.slice(1));
|
|
885
|
+
break;
|
|
886
|
+
case "--version":
|
|
887
|
+
case "-v":
|
|
888
|
+
console.log("0.1.0");
|
|
889
|
+
break;
|
|
890
|
+
case "--help":
|
|
891
|
+
case "-h":
|
|
892
|
+
case void 0:
|
|
893
|
+
console.log(HELP);
|
|
894
|
+
break;
|
|
895
|
+
default:
|
|
896
|
+
console.error(c4.red(`Unknown command: ${command}`));
|
|
897
|
+
console.log(HELP);
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
main().catch((err) => {
|
|
902
|
+
console.error(c4.red(`
|
|
903
|
+
\u2716 ${err.message}`));
|
|
904
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
905
|
+
process.exit(1);
|
|
906
|
+
});
|