create-line-harness 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.
Files changed (2) hide show
  1. package/dist/index.js +840 -0
  2. package/package.json +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,840 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { resolve } from "path";
5
+
6
+ // src/commands/setup.ts
7
+ import * as p10 from "@clack/prompts";
8
+ import pc from "picocolors";
9
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync5, existsSync as existsSync3 } from "fs";
10
+ import { join as join6 } from "path";
11
+
12
+ // src/steps/check-deps.ts
13
+ import * as p from "@clack/prompts";
14
+ import { execa } from "execa";
15
+ async function checkDeps() {
16
+ const s = p.spinner();
17
+ s.start("\u74B0\u5883\u30C1\u30A7\u30C3\u30AF\u4E2D...");
18
+ const nodeVersion = process.versions.node;
19
+ const major = parseInt(nodeVersion.split(".")[0], 10);
20
+ if (major < 20) {
21
+ s.stop("\u74B0\u5883\u30C1\u30A7\u30C3\u30AF\u5931\u6557");
22
+ p.cancel(`Node.js 20 \u4EE5\u4E0A\u304C\u5FC5\u8981\u3067\u3059\uFF08\u73FE\u5728: ${nodeVersion}\uFF09`);
23
+ process.exit(1);
24
+ }
25
+ try {
26
+ await execa("npx", ["--version"]);
27
+ } catch {
28
+ s.stop("\u74B0\u5883\u30C1\u30A7\u30C3\u30AF\u5931\u6557");
29
+ p.cancel("npx \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002Node.js \u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
30
+ process.exit(1);
31
+ }
32
+ s.stop("\u74B0\u5883\u30C1\u30A7\u30C3\u30AF\u5B8C\u4E86");
33
+ }
34
+
35
+ // src/steps/auth.ts
36
+ import * as p2 from "@clack/prompts";
37
+
38
+ // src/lib/wrangler.ts
39
+ import { execa as execa2 } from "execa";
40
+ var WranglerError = class extends Error {
41
+ constructor(message, stderr) {
42
+ super(message);
43
+ this.stderr = stderr;
44
+ this.name = "WranglerError";
45
+ }
46
+ };
47
+ async function wrangler(args2, options) {
48
+ try {
49
+ const result = await execa2("npx", ["wrangler", ...args2], {
50
+ cwd: options?.cwd,
51
+ input: options?.input,
52
+ env: { ...process.env, FORCE_COLOR: "0" }
53
+ });
54
+ return result.stdout;
55
+ } catch (error) {
56
+ throw new WranglerError(
57
+ `wrangler ${args2[0]} failed: ${error.stderr || error.message}`,
58
+ error.stderr || ""
59
+ );
60
+ }
61
+ }
62
+ async function wranglerInteractive(args2) {
63
+ await execa2("npx", ["wrangler", ...args2], {
64
+ stdio: "inherit",
65
+ env: { ...process.env, FORCE_COLOR: "1" }
66
+ });
67
+ }
68
+ async function isWranglerAuthenticated() {
69
+ try {
70
+ const output = await wrangler(["whoami"]);
71
+ return !output.toLowerCase().includes("not authenticated");
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ // src/steps/auth.ts
78
+ async function ensureAuth() {
79
+ const s = p2.spinner();
80
+ s.start("Cloudflare \u8A8D\u8A3C\u30C1\u30A7\u30C3\u30AF\u4E2D...");
81
+ const authenticated = await isWranglerAuthenticated();
82
+ if (authenticated) {
83
+ s.stop("Cloudflare \u8A8D\u8A3C\u6E08\u307F");
84
+ return;
85
+ }
86
+ s.stop("Cloudflare \u306B\u30ED\u30B0\u30A4\u30F3\u304C\u5FC5\u8981\u3067\u3059");
87
+ p2.log.info("\u30D6\u30E9\u30A6\u30B6\u304C\u958B\u304D\u307E\u3059\u3002Cloudflare \u306B\u30ED\u30B0\u30A4\u30F3\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
88
+ await wranglerInteractive(["login"]);
89
+ const nowAuthenticated = await isWranglerAuthenticated();
90
+ if (!nowAuthenticated) {
91
+ p2.cancel("Cloudflare \u30ED\u30B0\u30A4\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u8A66\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
92
+ process.exit(1);
93
+ }
94
+ p2.log.success("Cloudflare \u30ED\u30B0\u30A4\u30F3\u5B8C\u4E86");
95
+ }
96
+ async function getAccountId() {
97
+ const output = await wrangler(["whoami"]);
98
+ const match = output.match(/│\s+\S.*?\s+│\s+([a-f0-9]{32})\s+│/);
99
+ if (!match) {
100
+ throw new Error(
101
+ "Cloudflare \u30A2\u30AB\u30A6\u30F3\u30C8 ID \u3092\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3002wrangler whoami \u306E\u51FA\u529B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
102
+ );
103
+ }
104
+ return match[1];
105
+ }
106
+
107
+ // src/steps/prompt.ts
108
+ import * as p3 from "@clack/prompts";
109
+ async function promptLineCredentials() {
110
+ p3.log.step(
111
+ "LINE Developers Console \u3067\u30C1\u30E3\u30CD\u30EB\u60C5\u5831\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\nhttps://developers.line.biz/console/"
112
+ );
113
+ p3.log.info(
114
+ [
115
+ "\u5FC5\u8981\u306A\u30C1\u30E3\u30CD\u30EB\uFF082\u3064\uFF09:",
116
+ " 1. Messaging API \u30C1\u30E3\u30CD\u30EB \u2014 Bot\u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u9001\u53D7\u4FE1\u7528",
117
+ " 2. LINE Login \u30C1\u30E3\u30CD\u30EB \u2014 \u30E6\u30FC\u30B6\u30FC\u8A8D\u8A3C\u30FBLIFF\u7528",
118
+ "",
119
+ "\u307E\u3060\u4F5C\u3063\u3066\u3044\u306A\u3051\u308C\u3070\u3001\u4E0A\u306EURL\u304B\u3089\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
120
+ ].join("\n")
121
+ );
122
+ p3.log.message(
123
+ [
124
+ "\u2500\u2500 Messaging API \u2500\u2500",
125
+ "\u5834\u6240: LINE Official Account Manager \u2192 \u8A2D\u5B9A \u2192 Messaging API",
126
+ " https://manager.line.biz/ \u2192 \u30A2\u30AB\u30A6\u30F3\u30C8\u9078\u629E \u2192 \u8A2D\u5B9A \u2192 Messaging API"
127
+ ].join("\n")
128
+ );
129
+ const lineChannelId = await p3.text({
130
+ message: "Channel ID\uFF08\u6570\u5B57\uFF09",
131
+ placeholder: "\u540C\u3058\u30DA\u30FC\u30B8\u306B\u8868\u793A\u3055\u308C\u3066\u3044\u308B Channel ID",
132
+ validate(value) {
133
+ if (!value || !/^\d+$/.test(value.trim())) {
134
+ return "Channel ID \u306F\u6570\u5B57\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044";
135
+ }
136
+ }
137
+ });
138
+ if (p3.isCancel(lineChannelId)) {
139
+ p3.cancel("\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u3092\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
140
+ process.exit(0);
141
+ }
142
+ const lineChannelAccessToken = await p3.text({
143
+ message: "\u30C1\u30E3\u30CD\u30EB\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\uFF08\u9577\u671F\uFF09",
144
+ placeholder: "Messaging API\u8A2D\u5B9A \u2192 \u30C1\u30E3\u30CD\u30EB\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3 \u2192 \u767A\u884C",
145
+ validate(value) {
146
+ if (!value || value.trim().length < 10) {
147
+ return "\u30C1\u30E3\u30CD\u30EB\u30A2\u30AF\u30BB\u30B9\u30C8\u30FC\u30AF\u30F3\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044";
148
+ }
149
+ }
150
+ });
151
+ if (p3.isCancel(lineChannelAccessToken)) {
152
+ p3.cancel("\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u3092\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
153
+ process.exit(0);
154
+ }
155
+ const lineChannelSecret = await p3.text({
156
+ message: "\u30C1\u30E3\u30CD\u30EB\u30B7\u30FC\u30AF\u30EC\u30C3\u30C8",
157
+ placeholder: "\u30C1\u30E3\u30CD\u30EB\u57FA\u672C\u8A2D\u5B9A \u2192 \u30C1\u30E3\u30CD\u30EB\u30B7\u30FC\u30AF\u30EC\u30C3\u30C8",
158
+ validate(value) {
159
+ if (!value || value.trim().length < 10) {
160
+ return "\u30C1\u30E3\u30CD\u30EB\u30B7\u30FC\u30AF\u30EC\u30C3\u30C8\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044";
161
+ }
162
+ }
163
+ });
164
+ if (p3.isCancel(lineChannelSecret)) {
165
+ p3.cancel("\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u3092\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
166
+ process.exit(0);
167
+ }
168
+ p3.log.message(
169
+ [
170
+ "\u2500\u2500 LINE Login \u30C1\u30E3\u30CD\u30EB \u2500\u2500",
171
+ "\u5834\u6240: LINE Developers Console \u2192 \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC \u2192 LINE Login \u30C1\u30E3\u30CD\u30EB",
172
+ " https://developers.line.biz/console/",
173
+ " \u203B Messaging API \u3068\u306F\u5225\u306E\u30C1\u30E3\u30CD\u30EB\u3067\u3059"
174
+ ].join("\n")
175
+ );
176
+ const lineLoginChannelId = await p3.text({
177
+ message: "\u30C1\u30E3\u30CD\u30EB ID\uFF08\u6570\u5B57\uFF09",
178
+ placeholder: "\u30C1\u30E3\u30CD\u30EB\u57FA\u672C\u8A2D\u5B9A \u2192 \u30C1\u30E3\u30CD\u30EBID\uFF08\u4F8B: 2009554425\uFF09",
179
+ validate(value) {
180
+ if (!value || !/^\d+$/.test(value.trim())) {
181
+ return "\u30C1\u30E3\u30CD\u30EB ID \u306F\u6570\u5B57\u3067\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044";
182
+ }
183
+ }
184
+ });
185
+ if (p3.isCancel(lineLoginChannelId)) {
186
+ p3.cancel("\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u3092\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
187
+ process.exit(0);
188
+ }
189
+ return {
190
+ lineChannelId: lineChannelId.trim(),
191
+ lineChannelAccessToken: lineChannelAccessToken.trim(),
192
+ lineChannelSecret: lineChannelSecret.trim(),
193
+ lineLoginChannelId: lineLoginChannelId.trim()
194
+ };
195
+ }
196
+
197
+ // src/steps/database.ts
198
+ import * as p4 from "@clack/prompts";
199
+ import { readdirSync } from "fs";
200
+ import { join } from "path";
201
+ async function createDatabase(repoDir) {
202
+ const s = p4.spinner();
203
+ const databaseName = "line-harness";
204
+ s.start("D1 \u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u4F5C\u6210\u4E2D...");
205
+ let databaseId;
206
+ try {
207
+ const output = await wrangler(["d1", "create", databaseName]);
208
+ const tomlMatch = output.match(/database_id\s*=\s*"([^"]+)"/);
209
+ const jsonMatch = output.match(/"database_id"\s*:\s*"([^"]+)"/);
210
+ const uuidMatch = output.match(
211
+ /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
212
+ );
213
+ const match = tomlMatch || jsonMatch || uuidMatch;
214
+ if (!match) {
215
+ throw new Error(`D1 ID \u3092\u30D1\u30FC\u30B9\u3067\u304D\u307E\u305B\u3093: ${output}`);
216
+ }
217
+ databaseId = match[1];
218
+ s.stop("D1 \u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u4F5C\u6210\u5B8C\u4E86");
219
+ } catch (error) {
220
+ if (error instanceof WranglerError && error.stderr.includes("already exists")) {
221
+ s.stop("D1 \u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059");
222
+ const listOutput = await wrangler(["d1", "list", "--json"]);
223
+ const databases = JSON.parse(listOutput);
224
+ const db = databases.find(
225
+ (d) => d.name === databaseName
226
+ );
227
+ if (!db) {
228
+ throw new Error("\u65E2\u5B58\u306E D1 \u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093");
229
+ }
230
+ databaseId = db.uuid;
231
+ } else {
232
+ throw error;
233
+ }
234
+ }
235
+ const schemaFile = join(repoDir, "packages/db/schema.sql");
236
+ const migrationsDir = join(repoDir, "packages/db/migrations");
237
+ const migrationFiles = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
238
+ const totalFiles = 1 + migrationFiles.length;
239
+ s.start(`\u30C6\u30FC\u30D6\u30EB\u4F5C\u6210\u4E2D\uFF08${totalFiles} files\uFF09...`);
240
+ try {
241
+ await wrangler([
242
+ "d1",
243
+ "execute",
244
+ databaseName,
245
+ "--remote",
246
+ "--file",
247
+ schemaFile
248
+ ]);
249
+ } catch {
250
+ }
251
+ for (const file of migrationFiles) {
252
+ try {
253
+ await wrangler([
254
+ "d1",
255
+ "execute",
256
+ databaseName,
257
+ "--remote",
258
+ "--file",
259
+ join(migrationsDir, file)
260
+ ]);
261
+ } catch {
262
+ }
263
+ }
264
+ s.stop("\u30C6\u30FC\u30D6\u30EB\u4F5C\u6210\u5B8C\u4E86");
265
+ return { databaseId, databaseName };
266
+ }
267
+
268
+ // src/steps/deploy-worker.ts
269
+ import * as p5 from "@clack/prompts";
270
+ import { writeFileSync, existsSync, readFileSync } from "fs";
271
+ import { join as join2 } from "path";
272
+ async function deployWorker(options) {
273
+ const s = p5.spinner();
274
+ const workerDir = join2(options.repoDir, "apps/worker");
275
+ const tomlPath = join2(workerDir, "wrangler.toml");
276
+ const originalToml = existsSync(tomlPath) ? readFileSync(tomlPath, "utf-8") : null;
277
+ s.start("Worker \u30C7\u30D7\u30ED\u30A4\u4E2D...");
278
+ const deployToml = `name = "${options.workerName}"
279
+ main = "src/index.ts"
280
+ compatibility_date = "2024-12-01"
281
+ workers_dev = true
282
+ account_id = "${options.accountId}"
283
+
284
+ [[d1_databases]]
285
+ binding = "DB"
286
+ database_name = "${options.d1DatabaseName}"
287
+ database_id = "${options.d1DatabaseId}"
288
+
289
+ [triggers]
290
+ crons = ["*/5 * * * *"]
291
+ `;
292
+ writeFileSync(tomlPath, deployToml);
293
+ try {
294
+ const output = await wrangler(["deploy"], { cwd: workerDir });
295
+ const urlMatch = output.match(/(https:\/\/[^\s]+\.workers\.dev)/);
296
+ const workerUrl = urlMatch ? urlMatch[1] : `https://${options.workerName}.workers.dev`;
297
+ s.stop("Worker \u30C7\u30D7\u30ED\u30A4\u5B8C\u4E86");
298
+ return { workerUrl };
299
+ } finally {
300
+ if (originalToml) {
301
+ writeFileSync(tomlPath, originalToml);
302
+ }
303
+ }
304
+ }
305
+
306
+ // src/steps/deploy-admin.ts
307
+ import * as p6 from "@clack/prompts";
308
+ import { writeFileSync as writeFileSync2 } from "fs";
309
+ import { join as join3 } from "path";
310
+ import { execa as execa3 } from "execa";
311
+ async function deployAdmin(options) {
312
+ const s = p6.spinner();
313
+ const webDir = join3(options.repoDir, "apps/web");
314
+ s.start("Admin UI \u30D3\u30EB\u30C9\u4E2D...");
315
+ const envContent = `NEXT_PUBLIC_API_URL=${options.workerUrl}
316
+ `;
317
+ writeFileSync2(join3(webDir, ".env.production"), envContent);
318
+ try {
319
+ await execa3("pnpm", ["run", "build"], { cwd: webDir });
320
+ } catch (error) {
321
+ s.stop("Admin UI \u30D3\u30EB\u30C9\u5931\u6557");
322
+ throw new Error(`Admin UI \u306E\u30D3\u30EB\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error.message}`);
323
+ }
324
+ s.stop("Admin UI \u30D3\u30EB\u30C9\u5B8C\u4E86");
325
+ s.start("Admin UI \u30C7\u30D7\u30ED\u30A4\u4E2D...");
326
+ try {
327
+ try {
328
+ await wrangler(["pages", "project", "create", options.projectName, "--production-branch", "main"]);
329
+ } catch {
330
+ }
331
+ const output = await wrangler(
332
+ ["pages", "deploy", "out", "--project-name", options.projectName, "--commit-dirty=true"],
333
+ { cwd: webDir }
334
+ );
335
+ let adminUrl = `https://${options.projectName}.pages.dev`;
336
+ try {
337
+ const projectList = await wrangler(["pages", "project", "list"]);
338
+ const subdomainMatch = projectList.match(
339
+ new RegExp(`${options.projectName}\\s+\u2502\\s+(\\S+\\.pages\\.dev)`)
340
+ );
341
+ if (subdomainMatch) {
342
+ adminUrl = `https://${subdomainMatch[1]}`;
343
+ }
344
+ } catch {
345
+ }
346
+ s.stop("Admin UI \u30C7\u30D7\u30ED\u30A4\u5B8C\u4E86");
347
+ return { adminUrl };
348
+ } catch (error) {
349
+ s.stop("Admin UI \u30C7\u30D7\u30ED\u30A4\u5931\u6557");
350
+ throw new Error(`Admin UI \u306E\u30C7\u30D7\u30ED\u30A4\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error.message}`);
351
+ }
352
+ }
353
+
354
+ // src/steps/deploy-liff.ts
355
+ import * as p7 from "@clack/prompts";
356
+ import { writeFileSync as writeFileSync3 } from "fs";
357
+ import { join as join4 } from "path";
358
+ import { execa as execa4 } from "execa";
359
+ async function deployLiff(options) {
360
+ const s = p7.spinner();
361
+ const liffDir = join4(options.repoDir, "apps/liff");
362
+ s.start("LIFF \u30D3\u30EB\u30C9\u4E2D...");
363
+ const envContent = `VITE_API_URL=${options.workerUrl}
364
+ VITE_LIFF_ID=${options.liffId}
365
+ VITE_BOT_BASIC_ID=${options.botBasicId}
366
+ `;
367
+ writeFileSync3(join4(liffDir, ".env.production"), envContent);
368
+ try {
369
+ await execa4("pnpm", ["run", "build"], { cwd: liffDir });
370
+ } catch (error) {
371
+ s.stop("LIFF \u30D3\u30EB\u30C9\u5931\u6557");
372
+ throw new Error(`LIFF \u306E\u30D3\u30EB\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error.message}`);
373
+ }
374
+ s.stop("LIFF \u30D3\u30EB\u30C9\u5B8C\u4E86");
375
+ s.start("LIFF \u30C7\u30D7\u30ED\u30A4\u4E2D...");
376
+ try {
377
+ try {
378
+ await wrangler(["pages", "project", "create", options.projectName, "--production-branch", "main"]);
379
+ } catch {
380
+ }
381
+ const output = await wrangler(
382
+ ["pages", "deploy", "dist", "--project-name", options.projectName, "--commit-dirty=true"],
383
+ { cwd: liffDir }
384
+ );
385
+ let liffUrl = `https://${options.projectName}.pages.dev`;
386
+ try {
387
+ const projectList = await wrangler(["pages", "project", "list"]);
388
+ const subdomainMatch = projectList.match(
389
+ new RegExp(`${options.projectName}\\s+\u2502\\s+(\\S+\\.pages\\.dev)`)
390
+ );
391
+ if (subdomainMatch) {
392
+ liffUrl = `https://${subdomainMatch[1]}`;
393
+ }
394
+ } catch {
395
+ }
396
+ s.stop("LIFF \u30C7\u30D7\u30ED\u30A4\u5B8C\u4E86");
397
+ return { liffUrl };
398
+ } catch (error) {
399
+ s.stop("LIFF \u30C7\u30D7\u30ED\u30A4\u5931\u6557");
400
+ throw new Error(`LIFF \u306E\u30C7\u30D7\u30ED\u30A4\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error.message}`);
401
+ }
402
+ }
403
+
404
+ // src/steps/secrets.ts
405
+ import * as p8 from "@clack/prompts";
406
+ async function setSecrets(options) {
407
+ const s = p8.spinner();
408
+ s.start("\u30B7\u30FC\u30AF\u30EC\u30C3\u30C8\u8A2D\u5B9A\u4E2D...");
409
+ const secrets = {
410
+ LINE_CHANNEL_ACCESS_TOKEN: options.lineChannelAccessToken,
411
+ LINE_CHANNEL_SECRET: options.lineChannelSecret,
412
+ LINE_LOGIN_CHANNEL_ID: options.lineLoginChannelId,
413
+ LIFF_URL: `https://liff.line.me/${options.liffId}`,
414
+ API_KEY: options.apiKey
415
+ };
416
+ for (const [name, value] of Object.entries(secrets)) {
417
+ await wrangler(["secret", "put", name, "--name", options.workerName], {
418
+ input: value
419
+ });
420
+ }
421
+ s.stop("\u30B7\u30FC\u30AF\u30EC\u30C3\u30C8\u8A2D\u5B9A\u5B8C\u4E86");
422
+ }
423
+
424
+ // src/steps/mcp-config.ts
425
+ import * as p9 from "@clack/prompts";
426
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync4, existsSync as existsSync2 } from "fs";
427
+ import { join as join5 } from "path";
428
+ function generateMcpConfig(options) {
429
+ const mcpJsonPath = join5(process.cwd(), ".mcp.json");
430
+ const newServerConfig = {
431
+ command: "npx",
432
+ args: ["-y", "@line-harness/mcp-server@latest"],
433
+ env: {
434
+ LINE_HARNESS_API_URL: options.workerUrl,
435
+ LINE_HARNESS_API_KEY: options.apiKey
436
+ }
437
+ };
438
+ let mcpConfig = {};
439
+ if (existsSync2(mcpJsonPath)) {
440
+ try {
441
+ mcpConfig = JSON.parse(readFileSync2(mcpJsonPath, "utf-8"));
442
+ } catch {
443
+ }
444
+ }
445
+ if (!mcpConfig.mcpServers) {
446
+ mcpConfig.mcpServers = {};
447
+ }
448
+ mcpConfig.mcpServers["line-harness"] = newServerConfig;
449
+ writeFileSync4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n");
450
+ p9.log.success(".mcp.json \u306B MCP \u8A2D\u5B9A\u3092\u8FFD\u52A0\u3057\u307E\u3057\u305F");
451
+ }
452
+
453
+ // src/lib/crypto.ts
454
+ import { randomBytes } from "crypto";
455
+ function generateApiKey() {
456
+ return randomBytes(32).toString("hex");
457
+ }
458
+
459
+ // src/commands/setup.ts
460
+ function getStatePath(repoDir) {
461
+ return join6(repoDir, ".line-harness-setup.json");
462
+ }
463
+ function loadState(repoDir) {
464
+ const path = getStatePath(repoDir);
465
+ if (existsSync3(path)) {
466
+ try {
467
+ return JSON.parse(readFileSync3(path, "utf-8"));
468
+ } catch {
469
+ }
470
+ }
471
+ return { completedSteps: [] };
472
+ }
473
+ function saveState(repoDir, state) {
474
+ writeFileSync5(getStatePath(repoDir), JSON.stringify(state, null, 2) + "\n");
475
+ }
476
+ function isDone(state, step) {
477
+ return state.completedSteps.includes(step);
478
+ }
479
+ function markDone(state, step) {
480
+ if (!state.completedSteps.includes(step)) {
481
+ state.completedSteps.push(step);
482
+ }
483
+ }
484
+ async function runSetup(repoDir) {
485
+ p10.intro(pc.bgCyan(pc.black(" LINE Harness \u30BB\u30C3\u30C8\u30A2\u30C3\u30D7 ")));
486
+ const state = loadState(repoDir);
487
+ if (state.completedSteps.length > 0) {
488
+ p10.log.info(
489
+ `\u524D\u56DE\u306E\u9014\u4E2D\u304B\u3089\u518D\u958B\u3057\u307E\u3059\uFF08\u5B8C\u4E86\u6E08\u307F: ${state.completedSteps.join(", ")}\uFF09`
490
+ );
491
+ }
492
+ await checkDeps();
493
+ await ensureAuth();
494
+ if (!state.accountId) {
495
+ const accountId = await getAccountId();
496
+ state.accountId = accountId;
497
+ saveState(repoDir, state);
498
+ p10.log.success(`Cloudflare \u30A2\u30AB\u30A6\u30F3\u30C8: ${accountId}`);
499
+ }
500
+ if (!isDone(state, "credentials")) {
501
+ const credentials = await promptLineCredentials();
502
+ state.lineChannelId = credentials.lineChannelId;
503
+ state.lineChannelAccessToken = credentials.lineChannelAccessToken;
504
+ state.lineChannelSecret = credentials.lineChannelSecret;
505
+ state.lineLoginChannelId = credentials.lineLoginChannelId;
506
+ markDone(state, "credentials");
507
+ saveState(repoDir, state);
508
+ } else {
509
+ p10.log.success("LINE \u30C1\u30E3\u30CD\u30EB\u60C5\u5831: \u5165\u529B\u6E08\u307F\uFF08\u30B9\u30AD\u30C3\u30D7\uFF09");
510
+ }
511
+ if (!isDone(state, "liffId")) {
512
+ p10.log.message(
513
+ [
514
+ "\u2500\u2500 LIFF \u30A2\u30D7\u30EA\uFF08LINE Login \u30C1\u30E3\u30CD\u30EB\u5185\uFF09 \u2500\u2500",
515
+ "",
516
+ "LINE Login \u30C1\u30E3\u30CD\u30EB\u306E\u8A2D\u5B9A:",
517
+ " \u30EA\u30F3\u30AF\u3055\u308C\u305F\u30DC\u30C3\u30C8: Messaging API \u306E\u30DC\u30C3\u30C8\u3092\u9078\u629E",
518
+ " Scope: openid, profile, chat_message.write \u3092\u6709\u52B9\u5316",
519
+ " \u53CB\u3060\u3061\u8FFD\u52A0\u30AA\u30D7\u30B7\u30E7\u30F3: On (aggressive)",
520
+ "",
521
+ "LIFF \u30A2\u30D7\u30EA\u306E\u4F5C\u6210:",
522
+ " 1. LINE Login \u30C1\u30E3\u30CD\u30EB \u2192 LIFF \u30BF\u30D6 \u2192 \u8FFD\u52A0",
523
+ " 2. \u30B5\u30A4\u30BA: Full",
524
+ " 3. \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8 URL: https://example.com\uFF08\u5F8C\u3067\u5909\u66F4\u3057\u307E\u3059\uFF09",
525
+ " 4. \u4F5C\u6210\u5F8C\u306B\u8868\u793A\u3055\u308C\u308B LIFF ID \u3092\u30B3\u30D4\u30FC",
526
+ "",
527
+ "\u6CE8\u610F: LIFF \u30A2\u30D7\u30EA\u3092\u300C\u516C\u958B\u6E08\u307F\u300D\u306B\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u958B\u767A\u4E2D\u3060\u3068\u52D5\u304D\u307E\u305B\u3093\uFF09"
528
+ ].join("\n")
529
+ );
530
+ const liffId = await p10.text({
531
+ message: "LIFF ID",
532
+ placeholder: "\u30C1\u30E3\u30CD\u30EBID-\u30E9\u30F3\u30C0\u30E0\u6587\u5B57\u5217\uFF08\u4F8B: 2009554425-4IMBmLQ9\uFF09",
533
+ validate(value) {
534
+ if (!value || value.trim().length < 5) {
535
+ return "LIFF ID \u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044";
536
+ }
537
+ }
538
+ });
539
+ if (p10.isCancel(liffId)) {
540
+ p10.cancel("\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u3092\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
541
+ process.exit(0);
542
+ }
543
+ state.liffId = liffId.trim();
544
+ markDone(state, "liffId");
545
+ saveState(repoDir, state);
546
+ } else {
547
+ p10.log.success(`LIFF ID: \u5165\u529B\u6E08\u307F\uFF08${state.liffId}\uFF09`);
548
+ }
549
+ if (!state.apiKey) {
550
+ state.apiKey = generateApiKey();
551
+ saveState(repoDir, state);
552
+ }
553
+ if (!isDone(state, "database")) {
554
+ const { databaseId, databaseName } = await createDatabase(repoDir);
555
+ state.d1DatabaseId = databaseId;
556
+ state.d1DatabaseName = databaseName;
557
+ markDone(state, "database");
558
+ saveState(repoDir, state);
559
+ } else {
560
+ p10.log.success(`D1 \u30C7\u30FC\u30BF\u30D9\u30FC\u30B9: \u4F5C\u6210\u6E08\u307F\uFF08${state.d1DatabaseId}\uFF09`);
561
+ }
562
+ const workerName = "line-harness";
563
+ state.workerName = workerName;
564
+ if (!isDone(state, "worker")) {
565
+ const { workerUrl } = await deployWorker({
566
+ repoDir,
567
+ d1DatabaseId: state.d1DatabaseId,
568
+ d1DatabaseName: state.d1DatabaseName,
569
+ workerName,
570
+ accountId: state.accountId
571
+ });
572
+ state.workerUrl = workerUrl;
573
+ markDone(state, "worker");
574
+ saveState(repoDir, state);
575
+ } else {
576
+ p10.log.success(`Worker: \u30C7\u30D7\u30ED\u30A4\u6E08\u307F\uFF08${state.workerUrl}\uFF09`);
577
+ }
578
+ if (!isDone(state, "secrets")) {
579
+ await setSecrets({
580
+ workerName,
581
+ lineChannelAccessToken: state.lineChannelAccessToken,
582
+ lineChannelSecret: state.lineChannelSecret,
583
+ lineLoginChannelId: state.lineLoginChannelId,
584
+ liffId: state.liffId,
585
+ apiKey: state.apiKey
586
+ });
587
+ markDone(state, "secrets");
588
+ saveState(repoDir, state);
589
+ } else {
590
+ p10.log.success("\u30B7\u30FC\u30AF\u30EC\u30C3\u30C8: \u8A2D\u5B9A\u6E08\u307F");
591
+ }
592
+ if (!isDone(state, "lineAccount")) {
593
+ const s = p10.spinner();
594
+ s.start("LINE \u30A2\u30AB\u30A6\u30F3\u30C8\u767B\u9332\u4E2D...");
595
+ try {
596
+ const res = await fetch(`${state.workerUrl}/api/line-accounts`, {
597
+ method: "POST",
598
+ headers: {
599
+ Authorization: `Bearer ${state.apiKey}`,
600
+ "Content-Type": "application/json"
601
+ },
602
+ body: JSON.stringify({
603
+ name: "LINE Harness",
604
+ channelId: state.lineChannelId,
605
+ channelAccessToken: state.lineChannelAccessToken,
606
+ channelSecret: state.lineChannelSecret
607
+ })
608
+ });
609
+ if (res.ok) {
610
+ try {
611
+ await wrangler([
612
+ "d1",
613
+ "execute",
614
+ "line-harness",
615
+ "--remote",
616
+ "--command",
617
+ `UPDATE line_accounts SET login_channel_id = '${state.lineLoginChannelId}' WHERE channel_id = '${state.lineChannelId}'`
618
+ ]);
619
+ } catch {
620
+ }
621
+ try {
622
+ const botRes = await fetch("https://api.line.me/v2/bot/info", {
623
+ headers: { Authorization: `Bearer ${state.lineChannelAccessToken}` }
624
+ });
625
+ if (botRes.ok) {
626
+ const bot = await botRes.json();
627
+ if (bot.basicId) {
628
+ state.botBasicId = bot.basicId;
629
+ saveState(repoDir, state);
630
+ }
631
+ }
632
+ } catch {
633
+ }
634
+ s.stop("LINE \u30A2\u30AB\u30A6\u30F3\u30C8\u767B\u9332\u5B8C\u4E86");
635
+ } else {
636
+ const data = await res.json();
637
+ s.stop(`LINE \u30A2\u30AB\u30A6\u30F3\u30C8\u767B\u9332: ${data.error || "\u30A8\u30E9\u30FC"}`);
638
+ }
639
+ } catch {
640
+ s.stop("LINE \u30A2\u30AB\u30A6\u30F3\u30C8\u767B\u9332\u30B9\u30AD\u30C3\u30D7\uFF08Worker \u8D77\u52D5\u5F85\u3061\uFF09");
641
+ }
642
+ markDone(state, "lineAccount");
643
+ saveState(repoDir, state);
644
+ } else {
645
+ p10.log.success("LINE \u30A2\u30AB\u30A6\u30F3\u30C8: \u767B\u9332\u6E08\u307F");
646
+ }
647
+ const suffix = state.apiKey.slice(0, 8);
648
+ const adminProjectName = `lh-admin-${suffix}`;
649
+ if (!isDone(state, "admin")) {
650
+ const { adminUrl } = await deployAdmin({
651
+ repoDir,
652
+ workerUrl: state.workerUrl,
653
+ apiKey: state.apiKey,
654
+ projectName: adminProjectName
655
+ });
656
+ state.adminUrl = adminUrl;
657
+ markDone(state, "admin");
658
+ saveState(repoDir, state);
659
+ } else {
660
+ p10.log.success(`Admin UI: \u30C7\u30D7\u30ED\u30A4\u6E08\u307F\uFF08${state.adminUrl}\uFF09`);
661
+ }
662
+ const liffProjectName = `lh-liff-${suffix}`;
663
+ if (!isDone(state, "liff")) {
664
+ const { liffUrl } = await deployLiff({
665
+ repoDir,
666
+ workerUrl: state.workerUrl,
667
+ liffId: state.liffId,
668
+ botBasicId: state.botBasicId || "",
669
+ projectName: liffProjectName
670
+ });
671
+ state.liffUrl = liffUrl;
672
+ markDone(state, "liff");
673
+ saveState(repoDir, state);
674
+ } else {
675
+ p10.log.success(`LIFF: \u30C7\u30D7\u30ED\u30A4\u6E08\u307F\uFF08${state.liffUrl}\uFF09`);
676
+ }
677
+ const addMcp = await p10.confirm({
678
+ message: "MCP \u8A2D\u5B9A\u3092 .mcp.json \u306B\u8FFD\u52A0\u3057\u307E\u3059\u304B\uFF1F\uFF08Claude Code / Cursor \u7528\uFF09"
679
+ });
680
+ if (addMcp && !p10.isCancel(addMcp)) {
681
+ generateMcpConfig({ workerUrl: state.workerUrl, apiKey: state.apiKey });
682
+ }
683
+ p10.note(
684
+ [
685
+ `${pc.bold("\u2460 Webhook URL \u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044:")}`,
686
+ ` ${pc.cyan(`${state.workerUrl}/webhook`)}`,
687
+ ` \u2192 LINE Official Account Manager \u2192 \u8A2D\u5B9A \u2192 Messaging API`,
688
+ ` \u2192 Webhook URL \u306B\u8CBC\u308A\u4ED8\u3051 \u2192 \u300CWebhook\u306E\u5229\u7528\u300D\u3092 ${pc.bold("ON")} \u306B\u3059\u308B`,
689
+ "",
690
+ `${pc.bold("\u2461 LIFF \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8 URL \u3092\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044:")}`,
691
+ ` ${pc.cyan(state.liffUrl)}`,
692
+ ` \u2192 LINE Developers Console \u2192 LINE Login \u30C1\u30E3\u30CD\u30EB \u2192 LIFF`,
693
+ ` \u2192 \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8 URL \u3092\u3053\u306E URL \u306B\u5909\u66F4`,
694
+ "",
695
+ `${pc.bold("\u2462 \u53CB\u3060\u3061\u8FFD\u52A0 URL\uFF08\u3053\u306E URL \u3092\u5171\u6709\u3057\u3066\u304F\u3060\u3055\u3044\uFF09:")}`,
696
+ ` ${pc.cyan(`${state.workerUrl}/auth/line?ref=setup`)}`,
697
+ ` \u2192 QR \u3067\u76F4\u8FFD\u52A0\u3067\u306F\u306A\u304F\u3053\u306E URL \u7D4C\u7531\u3067\u8FFD\u52A0\u3057\u3066\u3082\u3089\u3046`,
698
+ "",
699
+ `${pc.bold("\u2463 \u7BA1\u7406\u753B\u9762:")}`,
700
+ ` ${pc.cyan(state.adminUrl)}`,
701
+ "",
702
+ `${pc.bold("API Key:")}`,
703
+ ` ${pc.dim(state.apiKey)}`,
704
+ ` \u2192 \u3053\u306E\u5024\u306F\u518D\u8868\u793A\u3067\u304D\u307E\u305B\u3093\u3002\u5B89\u5168\u306A\u5834\u6240\u306B\u4FDD\u5B58\u3057\u3066\u304F\u3060\u3055\u3044`
705
+ ].join("\n"),
706
+ "\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u5B8C\u4E86\uFF01"
707
+ );
708
+ const statePath = getStatePath(repoDir);
709
+ if (existsSync3(statePath)) {
710
+ const { unlinkSync } = await import("fs");
711
+ unlinkSync(statePath);
712
+ }
713
+ p10.outro(pc.green("LINE Harness \u3092\u4F7F\u3044\u59CB\u3081\u307E\u3057\u3087\u3046 \u{1F389}"));
714
+ }
715
+
716
+ // src/commands/update.ts
717
+ import * as p11 from "@clack/prompts";
718
+ import pc2 from "picocolors";
719
+ import { join as join7 } from "path";
720
+ import { execa as execa5 } from "execa";
721
+ async function runUpdate(repoDir) {
722
+ p11.intro(pc2.bgCyan(pc2.black(" LINE Harness \u30A2\u30C3\u30D7\u30C7\u30FC\u30C8 ")));
723
+ await ensureAuth();
724
+ const s = p11.spinner();
725
+ s.start("\u30DE\u30A4\u30B0\u30EC\u30FC\u30B7\u30E7\u30F3\u78BA\u8A8D\u4E2D...");
726
+ try {
727
+ await wrangler(
728
+ ["d1", "migrations", "apply", "line-harness", "--remote"],
729
+ { cwd: join7(repoDir, "packages/db") }
730
+ );
731
+ s.stop("\u30DE\u30A4\u30B0\u30EC\u30FC\u30B7\u30E7\u30F3\u5B8C\u4E86");
732
+ } catch {
733
+ s.stop("\u30DE\u30A4\u30B0\u30EC\u30FC\u30B7\u30E7\u30F3\u5B8C\u4E86\uFF08\u5909\u66F4\u306A\u3057\uFF09");
734
+ }
735
+ s.start("Worker \u518D\u30C7\u30D7\u30ED\u30A4\u4E2D...");
736
+ await wrangler(["deploy"], { cwd: join7(repoDir, "apps/worker") });
737
+ s.stop("Worker \u518D\u30C7\u30D7\u30ED\u30A4\u5B8C\u4E86");
738
+ s.start("Admin UI \u518D\u30C7\u30D7\u30ED\u30A4\u4E2D...");
739
+ const webDir = join7(repoDir, "apps/web");
740
+ await execa5("pnpm", ["run", "build"], { cwd: webDir });
741
+ await wrangler(
742
+ ["pages", "deploy", "out", "--project-name", "line-harness-admin"],
743
+ { cwd: webDir }
744
+ );
745
+ s.stop("Admin UI \u518D\u30C7\u30D7\u30ED\u30A4\u5B8C\u4E86");
746
+ s.start("LIFF \u518D\u30C7\u30D7\u30ED\u30A4\u4E2D...");
747
+ const liffDir = join7(repoDir, "apps/liff");
748
+ await execa5("pnpm", ["run", "build"], { cwd: liffDir });
749
+ await wrangler(
750
+ ["pages", "deploy", "dist", "--project-name", "line-harness-liff"],
751
+ { cwd: liffDir }
752
+ );
753
+ s.stop("LIFF \u518D\u30C7\u30D7\u30ED\u30A4\u5B8C\u4E86");
754
+ p11.outro(pc2.green("\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8\u5B8C\u4E86\uFF01"));
755
+ }
756
+
757
+ // src/steps/clone-repo.ts
758
+ import * as p12 from "@clack/prompts";
759
+ import { existsSync as existsSync4 } from "fs";
760
+ import { join as join8 } from "path";
761
+ import { tmpdir } from "os";
762
+ import { execa as execa6 } from "execa";
763
+ var REPO_URL = "https://github.com/Shudesu/line-harness.git";
764
+ async function ensureRepo(repoDir) {
765
+ if (repoDir && existsSync4(join8(repoDir, "pnpm-workspace.yaml"))) {
766
+ return repoDir;
767
+ }
768
+ if (existsSync4(join8(process.cwd(), "pnpm-workspace.yaml"))) {
769
+ return process.cwd();
770
+ }
771
+ const homeDir = join8(
772
+ process.env.HOME || process.env.USERPROFILE || tmpdir(),
773
+ ".line-harness"
774
+ );
775
+ if (existsSync4(join8(homeDir, "pnpm-workspace.yaml"))) {
776
+ const s2 = p12.spinner();
777
+ s2.start("\u6700\u65B0\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u53D6\u5F97\u4E2D...");
778
+ try {
779
+ await execa6("git", ["pull", "--ff-only"], { cwd: homeDir });
780
+ } catch {
781
+ }
782
+ s2.stop("\u30EA\u30DD\u30B8\u30C8\u30EA\u66F4\u65B0\u5B8C\u4E86");
783
+ return homeDir;
784
+ }
785
+ const s = p12.spinner();
786
+ s.start("LINE Harness \u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9\u4E2D...");
787
+ try {
788
+ await execa6("git", ["clone", "--depth", "1", REPO_URL, homeDir]);
789
+ } catch (error) {
790
+ s.stop("\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9\u5931\u6557");
791
+ throw new Error(
792
+ `git clone \u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${error.message}
793
+ git \u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3066\u3044\u308B\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
794
+ );
795
+ }
796
+ s.stop("\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9\u5B8C\u4E86");
797
+ s.start("\u4F9D\u5B58\u95A2\u4FC2\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u4E2D...");
798
+ try {
799
+ await execa6("npx", ["pnpm", "install", "--frozen-lockfile"], {
800
+ cwd: homeDir
801
+ });
802
+ } catch {
803
+ await execa6("npx", ["pnpm", "install"], { cwd: homeDir });
804
+ }
805
+ s.stop("\u4F9D\u5B58\u95A2\u4FC2\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u5B8C\u4E86");
806
+ return homeDir;
807
+ }
808
+
809
+ // src/index.ts
810
+ var args = process.argv.slice(2);
811
+ function parseArgs() {
812
+ let command = "setup";
813
+ let repoDir = null;
814
+ for (let i = 0; i < args.length; i++) {
815
+ if (args[i] === "--repo-dir" && args[i + 1]) {
816
+ repoDir = resolve(args[i + 1]);
817
+ i++;
818
+ } else if (!args[i].startsWith("-")) {
819
+ command = args[i];
820
+ }
821
+ }
822
+ return { command, repoDir };
823
+ }
824
+ async function main() {
825
+ const { command, repoDir: explicitRepoDir } = parseArgs();
826
+ const repoDir = await ensureRepo(explicitRepoDir);
827
+ if (command === "setup") {
828
+ await runSetup(repoDir);
829
+ } else if (command === "update") {
830
+ await runUpdate(repoDir);
831
+ } else {
832
+ console.error(`Unknown command: ${command}`);
833
+ console.error("Usage: create-line-harness [setup|update] [--repo-dir <path>]");
834
+ process.exit(1);
835
+ }
836
+ }
837
+ main().catch((error) => {
838
+ console.error("Error:", error.message);
839
+ process.exit(1);
840
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "create-line-harness",
3
+ "version": "0.1.0",
4
+ "description": "Set up LINE Harness — AI-operated LINE CRM — with one command",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-line-harness": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch"
15
+ },
16
+ "keywords": [
17
+ "line",
18
+ "crm",
19
+ "harness",
20
+ "cloudflare",
21
+ "workers",
22
+ "mcp",
23
+ "ai"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/Shudesu/line-harness.git",
28
+ "directory": "packages/create-line-harness"
29
+ },
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@clack/prompts": "^0.9.1",
33
+ "execa": "^9.5.2",
34
+ "picocolors": "^1.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "tsup": "^8.4.0",
38
+ "typescript": "^5.9.3",
39
+ "@types/node": "^22.0.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=20"
43
+ }
44
+ }