@apps-in-toss/web-framework 2.6.1 → 3.0.0-beta.3051978

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/dist/cli.js ADDED
@@ -0,0 +1,935 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Cli, Builtins } from "clipanion";
5
+
6
+ // src/cli/commands/build/index.ts
7
+ import * as p from "@clack/prompts";
8
+ import { Command } from "clipanion";
9
+ import path3 from "path";
10
+
11
+ // src/cli/commands/build/buildArtifact.ts
12
+ import { AppsInTossBundle, PlatformType } from "@apps-in-toss/ait-format";
13
+ import { cosmiconfig } from "cosmiconfig";
14
+ import { TypeScriptLoader } from "cosmiconfig-typescript-loader";
15
+ import path2 from "path";
16
+ import { readdir, readFile, writeFile } from "fs/promises";
17
+ import { uuidv7 } from "uuidv7";
18
+
19
+ // src/cli/utils/getPackageRoot.ts
20
+ import fs from "fs";
21
+ import path from "path";
22
+ function getPackageRoot(base) {
23
+ let dirPath = base;
24
+ const root = path.parse(dirPath).root;
25
+ while (dirPath !== root) {
26
+ if (fs.existsSync(path.join(dirPath, "package.json"))) {
27
+ return dirPath;
28
+ }
29
+ dirPath = path.dirname(dirPath);
30
+ }
31
+ return dirPath;
32
+ }
33
+
34
+ // src/cli/commands/build/buildArtifact.ts
35
+ async function buildArtifact() {
36
+ const projectRoot = getPackageRoot(process.cwd());
37
+ const projectPackageJsonData = await readFile(
38
+ path2.resolve(projectRoot, "package.json")
39
+ );
40
+ const projectPackageJson = JSON.parse(projectPackageJsonData.toString());
41
+ const sdkPackageJson = await getSDKPackageJson();
42
+ const appsInTossConfig = await getAppsInTossConfig();
43
+ const deploymentId = uuidv7();
44
+ const writer = AppsInTossBundle.writer({
45
+ deploymentId,
46
+ appName: appsInTossConfig.appName,
47
+ createdBy: sdkPackageJson.name
48
+ });
49
+ const unityMetadata = getUnityMetadata();
50
+ writer.setMetadata({
51
+ platform: PlatformType.WEB,
52
+ sdkVersion: sdkPackageJson.version,
53
+ packageJson: projectPackageJson,
54
+ extra: { unityMetadata, appsInTossConfig }
55
+ });
56
+ appsInTossConfig.permissions.forEach(
57
+ ({ name, access }) => writer.addPermission(name, access)
58
+ );
59
+ const webBundleDir = path2.resolve(
60
+ projectRoot,
61
+ appsInTossConfig.webBundleDir
62
+ );
63
+ const webBundleFiles = await getWebBundleFiles(webBundleDir);
64
+ for (const file of webBundleFiles) {
65
+ writer.addFile("sources/" + file.name, file.data);
66
+ }
67
+ const bundleJson = {
68
+ deploymentId,
69
+ byteLength: webBundleFiles.reduce(
70
+ (acc, cur) => acc + cur.data.byteLength,
71
+ 0
72
+ ),
73
+ sdk: { name: sdkPackageJson.name, version: sdkPackageJson.version },
74
+ config: appsInTossConfig
75
+ };
76
+ writer.addFile(
77
+ "bundle.json",
78
+ new Uint8Array(Buffer.from(JSON.stringify(bundleJson), "utf-8"))
79
+ );
80
+ writer.addFile(
81
+ "project-package.json",
82
+ new Uint8Array(projectPackageJsonData)
83
+ );
84
+ const artifactBuffer = await writer.toBuffer();
85
+ const outPath = path2.resolve(projectRoot, `${appsInTossConfig.appName}.ait`);
86
+ await writeFile(outPath, artifactBuffer);
87
+ return { deploymentId, outPath };
88
+ }
89
+ async function getSDKPackageJson() {
90
+ const packageJsonPath = path2.resolve(
91
+ getPackageRoot(import.meta.dirname),
92
+ "package.json"
93
+ );
94
+ const packageJsonString = (await readFile(packageJsonPath)).toString();
95
+ return JSON.parse(packageJsonString);
96
+ }
97
+ async function getAppsInTossConfig() {
98
+ const result = await cosmiconfig("apps-in-toss", {
99
+ loaders: {
100
+ ".ts": TypeScriptLoader(),
101
+ ".cts": TypeScriptLoader(),
102
+ ".mts": TypeScriptLoader()
103
+ },
104
+ searchPlaces: [
105
+ "apps-in-toss.config.ts",
106
+ "apps-in-toss.config.cts",
107
+ "apps-in-toss.config.mts",
108
+ "apps-in-toss.config.js",
109
+ "apps-in-toss.config.cjs",
110
+ "apps-in-toss.config.mjs"
111
+ ]
112
+ }).search(getPackageRoot(process.cwd()));
113
+ if (result == null) {
114
+ throw new Error("Config \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
115
+ }
116
+ return result.config;
117
+ }
118
+ function getUnityMetadata() {
119
+ const unityMetadataString = process.env.UNITY_METADATA;
120
+ if (typeof unityMetadataString !== "string") {
121
+ return null;
122
+ }
123
+ try {
124
+ return JSON.parse(unityMetadataString);
125
+ } catch (error) {
126
+ console.warn("UNITY_METADATA is not a valid JSON:", error);
127
+ return null;
128
+ }
129
+ }
130
+ async function getWebBundleFiles(webBundleDir) {
131
+ const filePaths = await getFilePathsInDir(webBundleDir);
132
+ const files = [];
133
+ for (const filePath of filePaths) {
134
+ const name = filePath.replace(webBundleDir + "/", "");
135
+ const data = new Uint8Array(await readFile(filePath));
136
+ files.push({ name, data });
137
+ }
138
+ return files;
139
+ }
140
+ async function getFilePathsInDir(dir) {
141
+ const entries = await readdir(dir, { withFileTypes: true });
142
+ const files = await Promise.all(
143
+ entries.map((entry) => {
144
+ const filePath = path2.resolve(dir, entry.name);
145
+ return entry.isDirectory() ? getFilePathsInDir(filePath) : filePath;
146
+ })
147
+ );
148
+ return files.flat();
149
+ }
150
+
151
+ // src/cli/commands/build/index.ts
152
+ var BuildCommand = class extends Command {
153
+ static paths = [["build"]];
154
+ static usage = Command.Usage({
155
+ category: "Build",
156
+ description: "\uC571\uC778\uD1A0\uC2A4 \uBC30\uD3EC\uC6A9 .ait \uC544\uD2F0\uD329\uD2B8\uB97C \uBE4C\uB4DC\uD569\uB2C8\uB2E4.",
157
+ examples: [["Build artifact", "apps-in-toss build"]]
158
+ });
159
+ async execute() {
160
+ const buildResult = await buildArtifact();
161
+ p.log.success(
162
+ `\uC571\uC778\uD1A0\uC2A4 \uBE4C\uB4DC\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. (${path3.basename(buildResult.outPath)})`
163
+ );
164
+ p.log.info(`deploymentId: ${buildResult.deploymentId}`);
165
+ }
166
+ };
167
+
168
+ // src/cli/commands/deploy/index.ts
169
+ import assert from "assert";
170
+ import fs4 from "fs";
171
+ import path5 from "path";
172
+ import { AppsInTossBundle as AppsInTossBundle2 } from "@apps-in-toss/ait-format";
173
+ import * as p2 from "@clack/prompts";
174
+ import { Command as Command2, Option } from "clipanion";
175
+
176
+ // src/cli/utils/colors.ts
177
+ function wrap(open, close) {
178
+ return (input) => `\x1B[${open}m${input}\x1B[${close}m`;
179
+ }
180
+ var colors = {
181
+ cyan: wrap(36, 39),
182
+ green: wrap(32, 39),
183
+ underline: wrap(4, 24)
184
+ };
185
+
186
+ // src/cli/utils/TokenStorage.ts
187
+ import fs2 from "fs";
188
+ import os from "os";
189
+ import path4 from "path";
190
+ var TokenStorage = class {
191
+ static get path() {
192
+ const home = os.homedir();
193
+ const dir = path4.join(home, ".ait");
194
+ if (!fs2.existsSync(dir)) {
195
+ fs2.mkdirSync(dir, { recursive: true });
196
+ }
197
+ return path4.join(dir, "credentials");
198
+ }
199
+ static read() {
200
+ const file = this.path;
201
+ if (!fs2.existsSync(file)) {
202
+ return {};
203
+ }
204
+ const raw = fs2.readFileSync(file, "utf8");
205
+ try {
206
+ const parsed = JSON.parse(raw);
207
+ if (parsed && typeof parsed === "object") {
208
+ return parsed;
209
+ }
210
+ } catch {
211
+ }
212
+ return {};
213
+ }
214
+ static write(map) {
215
+ const file = this.path;
216
+ const content = JSON.stringify(map, null, 2);
217
+ fs2.writeFileSync(file, content, { encoding: "utf8" });
218
+ }
219
+ static set(workspace, token) {
220
+ const creds = this.read();
221
+ creds[workspace] = token;
222
+ this.write(creds);
223
+ }
224
+ static delete(workspace) {
225
+ const creds = this.read();
226
+ if (workspace in creds) {
227
+ delete creds[workspace];
228
+ this.write(creds);
229
+ return true;
230
+ }
231
+ return false;
232
+ }
233
+ static get(workspace) {
234
+ const creds = this.read();
235
+ return creds[workspace];
236
+ }
237
+ };
238
+
239
+ // src/cli/utils/upload.ts
240
+ import fs3 from "fs";
241
+
242
+ // src/cli/utils/constants.ts
243
+ var DEFAULT_API_BASE_URL = "https://apps-in-toss.toss.im/console";
244
+
245
+ // src/cli/utils/flowAsync.ts
246
+ function flowAsync(...funcs) {
247
+ return async function(...args) {
248
+ let result = funcs.length ? await funcs[0].apply(this, args) : args[0];
249
+ for (let i = 1; i < funcs.length; i++) {
250
+ result = await funcs[i]?.call(this, result);
251
+ }
252
+ return result;
253
+ };
254
+ }
255
+
256
+ // src/cli/utils/handleFetchResponse.ts
257
+ async function handleFetchResponse(response) {
258
+ if (!response.ok) {
259
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
260
+ }
261
+ const data = await response.clone().json();
262
+ if (data.resultType !== "SUCCESS") {
263
+ const errorCode = data?.error?.errorCode ?? "-1";
264
+ const errorReason = data?.error?.reason ?? "unknown";
265
+ throw new Error(`${errorReason} (Code: ${errorCode})`);
266
+ }
267
+ return data.success;
268
+ }
269
+
270
+ // src/cli/utils/withRetry.ts
271
+ import { setTimeout as sleep } from "timers/promises";
272
+ async function withRetry(fn, options = {}) {
273
+ const {
274
+ maxRetries = 60,
275
+ delayMs = 1e4,
276
+ shouldRetryOnResult,
277
+ shouldRetryOnError,
278
+ onRetry
279
+ } = options;
280
+ let attempt = 0;
281
+ while (attempt < maxRetries) {
282
+ try {
283
+ const result = await fn();
284
+ if (shouldRetryOnResult && shouldRetryOnResult(result)) {
285
+ onRetry?.(attempt + 1, { result });
286
+ await sleep(delayMs);
287
+ attempt++;
288
+ continue;
289
+ }
290
+ return result;
291
+ } catch (error) {
292
+ const retryable = shouldRetryOnError ? shouldRetryOnError(error) : false;
293
+ if (!retryable) {
294
+ throw error;
295
+ }
296
+ onRetry?.(attempt + 1, { error });
297
+ await sleep(delayMs);
298
+ attempt++;
299
+ }
300
+ }
301
+ throw new Error("\uCD5C\uB300 \uB300\uAE30\uC2DC\uAC04\uC744 \uCD08\uACFC\uD588\uC5B4\uC694.");
302
+ }
303
+
304
+ // src/cli/utils/upload.ts
305
+ async function uploadArtifact(config) {
306
+ return flowAsync(
307
+ uploadStart(),
308
+ uploadArtifactToRemote(),
309
+ uploadComplete(),
310
+ checkBundleStatus()
311
+ )(config);
312
+ }
313
+ function uploadStart() {
314
+ return async (config) => {
315
+ const baseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL;
316
+ const response = await fetch(
317
+ `${baseUrl}/api-public/v3/openapi/bundles/${config.appName}/upload-start`,
318
+ {
319
+ method: "POST",
320
+ headers: {
321
+ "Content-Type": "application/json",
322
+ Authorization: `Bearer ${config.apiKey}`
323
+ },
324
+ body: JSON.stringify({
325
+ deploymentId: config.deploymentId,
326
+ memo: config.memo
327
+ })
328
+ }
329
+ );
330
+ return {
331
+ config,
332
+ output: await handleFetchResponse(response)
333
+ };
334
+ };
335
+ }
336
+ function uploadArtifactToRemote() {
337
+ return async (params) => {
338
+ const { config, output } = params;
339
+ const stat = await fs3.promises.stat(config.artifactPath);
340
+ const stream = fs3.createReadStream(config.artifactPath);
341
+ const response = await fetch(output.uploadUrl, {
342
+ method: "PUT",
343
+ headers: {
344
+ "Content-Type": "application/zip",
345
+ "Content-Length": String(stat.size)
346
+ },
347
+ // Node.js의 fetch는 ReadStream을 body로 받을 수 있으나 DOM 타입에는 없어 캐스팅합니다.
348
+ body: stream,
349
+ duplex: "half"
350
+ });
351
+ if (!response.ok) {
352
+ throw new Error("\uC5C5\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.");
353
+ }
354
+ return config;
355
+ };
356
+ }
357
+ function uploadComplete() {
358
+ return async (config) => {
359
+ const baseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL;
360
+ const response = await fetch(
361
+ `${baseUrl}/api-public/v3/openapi/bundles/${config.appName}/upload-complete`,
362
+ {
363
+ method: "POST",
364
+ headers: {
365
+ "Content-Type": "application/json",
366
+ Authorization: `Bearer ${config.apiKey}`
367
+ },
368
+ body: JSON.stringify({
369
+ deploymentId: config.deploymentId
370
+ })
371
+ }
372
+ );
373
+ await handleFetchResponse(response);
374
+ return config;
375
+ };
376
+ }
377
+ function checkBundleStatus(delayMs = 1e4, maxRetries = 10) {
378
+ return async (config) => {
379
+ const baseUrl = config.baseUrl ?? DEFAULT_API_BASE_URL;
380
+ function assertBuildNotFailed(config2, reviewStatus2) {
381
+ if (reviewStatus2 === "BUILD_FAILED") {
382
+ throw new Error(
383
+ `\uBE4C\uB4DC\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uCF58\uC194\uC5D0\uC11C \uBE4C\uB4DC \uC2E4\uD328 \uC0AC\uC720\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.(deploymentId: ${config2.deploymentId})`
384
+ );
385
+ }
386
+ }
387
+ async function pollBundleStatus() {
388
+ const response = await fetch(
389
+ `${baseUrl}/api-public/v3/openapi/bundles/${config.appName}/deployments/${config.deploymentId}`,
390
+ {
391
+ method: "GET",
392
+ headers: {
393
+ "Content-Type": "application/json",
394
+ Authorization: `Bearer ${config.apiKey}`
395
+ }
396
+ }
397
+ );
398
+ const { reviewStatus: reviewStatus2 } = await handleFetchResponse(response);
399
+ return reviewStatus2;
400
+ }
401
+ const reviewStatus = await withRetry(pollBundleStatus, {
402
+ maxRetries,
403
+ delayMs,
404
+ shouldRetryOnResult: (reviewStatus2) => reviewStatus2 === "BUILDING" || reviewStatus2 === "PREPARE",
405
+ shouldRetryOnError: () => false
406
+ });
407
+ assertBuildNotFailed(config, reviewStatus);
408
+ };
409
+ }
410
+
411
+ // src/cli/commands/deploy/index.ts
412
+ var DeployCommand = class extends Command2 {
413
+ static paths = [["deploy"]];
414
+ static usage = Command2.Usage({
415
+ category: "Deploy",
416
+ description: "\uBE4C\uB4DC\uB41C .ait \uC544\uD2F0\uD329\uD2B8\uB97C \uC571\uC778\uD1A0\uC2A4\uC5D0 \uC5C5\uB85C\uB4DC(\uBC30\uD3EC)\uD569\uB2C8\uB2E4.",
417
+ examples: [
418
+ ["\uAE30\uBCF8 \uC544\uD2F0\uD329\uD2B8(.ait) \uBC30\uD3EC", "apps-in-toss deploy"],
419
+ [
420
+ "\uD2B9\uC815 .ait \uD30C\uC77C \uBC30\uD3EC",
421
+ "apps-in-toss deploy --location ./dist/my-app.ait"
422
+ ],
423
+ ["\uD504\uB85C\uD544\uC5D0 \uC800\uC7A5\uB41C \uD1A0\uD070\uC73C\uB85C \uBC30\uD3EC", "apps-in-toss deploy --profile dev"],
424
+ [
425
+ "\uCD9C\uC2DC \uBA54\uBAA8\uC640 \uD568\uAED8 \uBC30\uD3EC",
426
+ 'apps-in-toss deploy -m "\uCD9C\uC2DC \uBA54\uBAA8(\uCD5C\uB300 1000\uC790)"'
427
+ ],
428
+ ["\uBC30\uD3EC \uACB0\uACFC scheme\uB9CC \uCD9C\uB825", "apps-in-toss deploy --scheme-only"]
429
+ ]
430
+ });
431
+ apiKey = Option.String("--api-key", {
432
+ required: false,
433
+ description: "\uBC30\uD3EC\uB97C \uC704\uD55C API \uD0A4\uC785\uB2C8\uB2E4. \uBA85\uC2DC\uC801\uC73C\uB85C \uC774 \uC635\uC158\uC73C\uB85C api key\uB97C \uC9C1\uC811 \uC785\uB825\uD558\uAC70\uB098, --profile \uC635\uC158\uC744 \uC0AC\uC6A9\uD574\uC11C \uD504\uB85C\uD544\uC744 \uC9C0\uC815\uD558\uBA74 \uD504\uB85C\uD544\uC5D0 \uB4F1\uB85D\uB41C api key\uB97C \uC0AC\uC6A9\uD574\uC694."
434
+ });
435
+ workspace = Option.String("--workspace", {
436
+ required: false,
437
+ description: "(deprecated) \uD1A0\uD070 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uC774\uB984\uC785\uB2C8\uB2E4. \uC774 \uC635\uC158 \uB300\uC2E0 --profile \uC635\uC158\uC744 \uC0AC\uC6A9\uD574\uC8FC\uC138\uC694."
438
+ });
439
+ profile = Option.String("--profile", {
440
+ required: false,
441
+ description: "apps-in-toss token add \uBA85\uB839\uC5B4\uB97C \uD1B5\uD574 \uB4F1\uB85D\uD55C \uD504\uB85C\uD544\uC774\uC5D0\uC694. \uD504\uB85C\uD544\uC774 \uC5C6\uC73C\uBA74 default \uD504\uB85C\uD544\uC744 \uC0AC\uC6A9\uD574\uC694."
442
+ });
443
+ baseUrl = Option.String("--base-url", {
444
+ description: "API Base URL",
445
+ required: false
446
+ });
447
+ location = Option.String("--location", {
448
+ description: "\uC5C5\uB85C\uB4DC\uD560 .ait \uD30C\uC77C\uC758 \uACBD\uB85C\uB97C \uBA85\uC2DC\uC801\uC73C\uB85C \uC9C0\uC815\uD574\uC694. \uAE30\uBCF8\uAC12\uC740 \uD604\uC7AC \uD328\uD0A4\uC9C0 \uB8E8\uD2B8 \uB514\uB809\uD1A0\uB9AC\uC5D0 \uC788\uB294 ait \uD30C\uC77C\uC774\uC5D0\uC694.",
449
+ required: false
450
+ });
451
+ schemeOnly = Option.Boolean("--scheme-only", {
452
+ required: false,
453
+ description: "\uBC30\uD3EC \uACB0\uACFC\uB97C intoss-private scheme\uB9CC \uCD9C\uB825\uD558\uB3C4\uB85D \uC124\uC815\uD574\uC694. \uAE30\uBCF8\uAC12\uC740 false\uC608\uC694."
454
+ });
455
+ memo = Option.String("-m,--memo", {
456
+ required: false,
457
+ description: "\uCD9C\uC2DC \uBA54\uBAA8\uB97C \uC785\uB825\uD574\uC694. \uCD5C\uB300 1000\uC790\uAE4C\uC9C0 \uC785\uB825\uD560 \uC218 \uC788\uC5B4\uC694."
458
+ });
459
+ async execute() {
460
+ const baseUrl = this.baseUrl;
461
+ const profile = this.profile || this.workspace || "default";
462
+ if (this.workspace) {
463
+ p2.log.warn(
464
+ "(deprecated) --workspace \uC635\uC158\uC740 \uC774\uC81C \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uC544\uC694. \uC774 \uC635\uC158 \uB300\uC2E0 --profile \uC635\uC158\uC744 \uC0AC\uC6A9\uD574\uC8FC\uC138\uC694."
465
+ );
466
+ }
467
+ const apiKey = await this.getApiKey(profile);
468
+ if (p2.isCancel(apiKey)) {
469
+ return;
470
+ }
471
+ try {
472
+ if (this.memo != null && this.memo.length > 1e3) {
473
+ throw new Error("memo\uB294 \uCD5C\uB300 1000\uC790\uAE4C\uC9C0 \uC785\uB825\uD560 \uC218 \uC788\uC5B4\uC694.");
474
+ }
475
+ const resolvedArtifactPath = this.getArtifactPath(this.location);
476
+ const buffer = fs4.readFileSync(resolvedArtifactPath);
477
+ const format = AppsInTossBundle2.detect(buffer);
478
+ if (format !== AppsInTossBundle2.Format.AIT) {
479
+ throw new Error("\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4");
480
+ }
481
+ const reader = AppsInTossBundle2.reader(buffer);
482
+ const appName = reader.appName;
483
+ const deploymentId = reader.deploymentId;
484
+ assert(typeof appName === "string", "ait \uD30C\uC77C\uC5D0 appName\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
485
+ assert(
486
+ typeof deploymentId === "string",
487
+ "ait \uD30C\uC77C\uC5D0 deploymentId\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4"
488
+ );
489
+ const runtimeVersion = reader.metadata?.runtimeVersion;
490
+ assert(
491
+ typeof runtimeVersion === "string" && runtimeVersion.length > 0,
492
+ "ait \uD30C\uC77C\uC5D0 runtimeVersion\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBC88\uB4E4\uC744 \uB2E4\uC2DC \uBE4C\uB4DC\uD574\uC8FC\uC138\uC694."
493
+ );
494
+ const colorAppName = this.decorate(appName, colors.cyan);
495
+ if (this.schemeOnly) {
496
+ await uploadArtifact({
497
+ artifactPath: resolvedArtifactPath,
498
+ appName,
499
+ deploymentId,
500
+ apiKey,
501
+ memo: this.memo,
502
+ baseUrl
503
+ });
504
+ } else {
505
+ await p2.tasks([
506
+ {
507
+ title: `${colorAppName} \uC571 \uBC30\uD3EC \uC911...`,
508
+ task: async () => {
509
+ await uploadArtifact({
510
+ artifactPath: resolvedArtifactPath,
511
+ appName,
512
+ deploymentId,
513
+ apiKey,
514
+ memo: this.memo,
515
+ baseUrl
516
+ });
517
+ return `${colorAppName} \uBC30\uD3EC\uAC00 \uC644\uB8CC\uB418\uC5C8\uC5B4\uC694`;
518
+ }
519
+ }
520
+ ]);
521
+ }
522
+ this.printResult(appName, deploymentId);
523
+ } catch (error) {
524
+ if (error instanceof Error) {
525
+ this.printError(error);
526
+ } else {
527
+ console.error(error);
528
+ }
529
+ process.exit(1);
530
+ }
531
+ }
532
+ decorate(message, color) {
533
+ if (this.schemeOnly) {
534
+ return message;
535
+ }
536
+ return colors.underline(color(message));
537
+ }
538
+ printResult(appName, deploymentId) {
539
+ const result = `intoss-private://${appName}?_deploymentId=${deploymentId}`;
540
+ if (this.schemeOnly) {
541
+ this.context.stdout.write(`${result}
542
+ `);
543
+ } else {
544
+ p2.note(this.decorate(result, colors.green));
545
+ }
546
+ }
547
+ printError(error) {
548
+ if (this.schemeOnly) {
549
+ this.context.stdout.write(`${error.message}
550
+ `);
551
+ } else {
552
+ p2.log.error(error.message);
553
+ }
554
+ }
555
+ async getApiKey(profile) {
556
+ const token = TokenStorage.get(profile) || this.apiKey;
557
+ if (token) {
558
+ return token;
559
+ }
560
+ return await p2.password({
561
+ message: "\uC571\uC778\uD1A0\uC2A4 \uBC30\uD3EC API \uD0A4\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694",
562
+ validate: (value) => {
563
+ if (value == null || value.length === 0) {
564
+ return "API \uD0A4\uB294 \uD544\uC218 \uC785\uB825 \uD56D\uBAA9\uC785\uB2C8\uB2E4.";
565
+ }
566
+ return;
567
+ }
568
+ });
569
+ }
570
+ getArtifactPath(location) {
571
+ if (location) {
572
+ if (!location.endsWith(".ait")) {
573
+ throw new Error("\uBC30\uD3EC\uD560 \uD30C\uC77C\uC740 .ait \uD655\uC7A5\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4.");
574
+ }
575
+ return path5.resolve(location);
576
+ }
577
+ const packageRoot = getPackageRoot(process.cwd());
578
+ const artifactFile = fs4.readdirSync(packageRoot).find((file) => file.endsWith(".ait"));
579
+ if (!artifactFile) {
580
+ throw new Error("\uBC30\uD3EC\uD560 .ait \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
581
+ }
582
+ return path5.resolve(packageRoot, artifactFile);
583
+ }
584
+ };
585
+
586
+ // src/cli/commands/init/index.ts
587
+ import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
588
+ import { join } from "path";
589
+ import * as p3 from "@clack/prompts";
590
+ import { Command as Command3, Option as Option2 } from "clipanion";
591
+
592
+ // src/cli/utils/ensureSelect.ts
593
+ async function ensureSelect({
594
+ value,
595
+ prompt
596
+ }) {
597
+ if (value) {
598
+ return value;
599
+ }
600
+ return await prompt();
601
+ }
602
+
603
+ // src/cli/utils/kebabCase.ts
604
+ function kebabCase(str) {
605
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
606
+ }
607
+
608
+ // src/cli/utils/transformTemplate.ts
609
+ function transformTemplate(templateString, values) {
610
+ let result = templateString;
611
+ for (const key in values) {
612
+ const placeholder = `%%${key}%%`;
613
+ result = result.replace(new RegExp(placeholder, "g"), values[key]);
614
+ }
615
+ return result;
616
+ }
617
+
618
+ // src/cli/commands/init/templates.ts
619
+ var WEB_FRAMEWORK_CONFIG_TEMPLATE = `import { defineConfig } from '@apps-in-toss/web-framework/config';
620
+
621
+ export default defineConfig({
622
+ appName: '%%appName%%',
623
+ brand: {
624
+ primaryColor: '#3182F6', // \uD654\uBA74\uC5D0 \uB178\uCD9C\uB420 \uC571\uC758 \uAE30\uBCF8 \uC0C9\uC0C1\uC73C\uB85C \uBC14\uAFD4\uC8FC\uC138\uC694.
625
+ },
626
+ permissions: [],
627
+ webBundleDir: '%%webBundleDir%%',
628
+ });
629
+ `;
630
+
631
+ // src/cli/commands/init/index.ts
632
+ async function templateWebFramework({
633
+ appName,
634
+ cwd,
635
+ skipInput
636
+ }) {
637
+ const packageJsonPath = join(cwd, "package.json");
638
+ const packageJsonRaw = await readFile2(packageJsonPath, {
639
+ encoding: "utf-8"
640
+ });
641
+ const packageJson = JSON.parse(packageJsonRaw);
642
+ packageJson.scripts ??= {};
643
+ if (packageJson.scripts.build) {
644
+ packageJson.scripts.build = packageJson.scripts.build + " && ait build";
645
+ }
646
+ packageJson.scripts.deploy = "ait deploy";
647
+ await writeFile2(packageJsonPath, JSON.stringify(packageJson, null, 2), {
648
+ encoding: "utf-8"
649
+ });
650
+ p3.log.step(".gitignore \uD30C\uC77C\uC744 \uC5C5\uB370\uC774\uD2B8\uD558\uB294 \uC911...");
651
+ await appendFile(join(cwd, ".gitignore"), "\n*.ait\n");
652
+ const webBundleDir = skipInput ? "dist" : await p3.text({
653
+ message: "\uC6F9 \uBC88\uB4E4 \uACB0\uACFC\uBB3C\uC774 \uC704\uCE58\uD55C \uB514\uB809\uD1A0\uB9AC\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.",
654
+ placeholder: "dist",
655
+ defaultValue: "dist"
656
+ });
657
+ if (p3.isCancel(webBundleDir)) {
658
+ p3.cancel("\uCD08\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
659
+ return;
660
+ }
661
+ p3.log.step("apps-in-toss.config.ts \uD30C\uC77C\uC744 \uC0DD\uC131\uD558\uB294 \uC911...");
662
+ const config = transformTemplate(WEB_FRAMEWORK_CONFIG_TEMPLATE, {
663
+ appName,
664
+ webBundleDir
665
+ });
666
+ await writeFile2(join(cwd, "apps-in-toss.config.ts"), config, {
667
+ encoding: "utf-8"
668
+ });
669
+ }
670
+ var InitCommand = class extends Command3 {
671
+ static paths = [["init"]];
672
+ static usage = Command3.Usage({
673
+ category: "Setup",
674
+ description: "\uC571\uC778\uD1A0\uC2A4 \uC6F9 \uD504\uB808\uC784\uC6CC\uD06C \uAC1C\uBC1C/\uBC30\uD3EC \uC124\uC815\uC744 \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4.",
675
+ examples: [
676
+ ["\uB300\uD654\uD615\uC73C\uB85C \uCD08\uAE30\uD654", "apps-in-toss init"],
677
+ ["\uC571 \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC5EC \uCD08\uAE30\uD654", "apps-in-toss init --app-name my-miniapp"]
678
+ ]
679
+ });
680
+ appName = Option2.String("--app-name", {
681
+ required: false,
682
+ description: "\uC571 \uC774\uB984(\uCF00\uBC25-\uCF00\uC774\uC2A4)\uC744 \uC9C0\uC815\uD574\uC694. \uC608) my-miniapp"
683
+ });
684
+ skipInput = Option2.Boolean("--skip-input", { required: false, hidden: true });
685
+ async execute() {
686
+ const cwd = getPackageRoot(process.cwd());
687
+ p3.intro("\u{1F680} \uC571 \uCD08\uAE30\uD654\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4");
688
+ const appName = await ensureSelect({
689
+ value: this.appName,
690
+ prompt: async () => p3.text({
691
+ message: "Enter app name",
692
+ validate: (value) => {
693
+ if (!value) {
694
+ return "\uC571 \uC774\uB984\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694";
695
+ }
696
+ const kebabCaseValue = kebabCase(value);
697
+ if (value !== kebabCaseValue) {
698
+ return `\uC571 \uC774\uB984\uC740 \uCF00\uBC25-\uCF00\uC774\uC2A4 \uD615\uC2DD\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4 (\uC608\uC2DC: ${kebabCaseValue})`;
699
+ }
700
+ return;
701
+ }
702
+ })
703
+ });
704
+ if (p3.isCancel(appName)) {
705
+ p3.cancel("\uCD08\uAE30\uD654\uAC00 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4");
706
+ return;
707
+ }
708
+ p3.log.step(`\uC571 \uC774\uB984\uC774 '${appName}'\uC73C\uB85C \uC124\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4`);
709
+ await templateWebFramework({
710
+ appName,
711
+ cwd,
712
+ skipInput: this.skipInput ?? false
713
+ });
714
+ p3.outro("\u2728 \uCD08\uAE30\uD654\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!");
715
+ }
716
+ };
717
+
718
+ // src/cli/commands/migration/index.ts
719
+ import * as p5 from "@clack/prompts";
720
+ import { Command as Command4, Option as Option3 } from "clipanion";
721
+
722
+ // src/cli/commands/migration/web-framework-v3.ts
723
+ import * as p4 from "@clack/prompts";
724
+ import { cosmiconfig as cosmiconfig2 } from "cosmiconfig";
725
+ import { TypeScriptLoader as TypeScriptLoader2 } from "cosmiconfig-typescript-loader";
726
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
727
+ import { resolve } from "path";
728
+ import jscodeshift from "jscodeshift";
729
+ async function migrateWebFrameworkV3() {
730
+ const projectRoot = getPackageRoot(process.cwd());
731
+ p4.log.info("@apps-in-toss/web-framework V3 \uC790\uB3D9 \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4.");
732
+ p4.log.info(
733
+ "\uC790\uB3D9 \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC5D0 \uC2E4\uD328\uD560 \uACBD\uC6B0, \uCEE4\uBBA4\uB2C8\uD2F0\uC5D0\uC11C \uC218\uB3D9 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uAC00\uC774\uB4DC\uB97C \uCC38\uACE0\uD574\uC8FC\uC138\uC694.\nhttps://techchat-apps-in-toss.toss.im"
734
+ );
735
+ const { config: graniteConfig, filepath: graniteConfigPath } = await getGraniteConfig(projectRoot);
736
+ await migrateAppsInTossConfig(
737
+ graniteConfigPath,
738
+ resolve(
739
+ projectRoot,
740
+ "apps-in-toss.config." + graniteConfigPath.split(".").pop()
741
+ )
742
+ );
743
+ await migratePackageJsonScripts(
744
+ resolve(projectRoot, "package.json"),
745
+ graniteConfig.web.commands.dev,
746
+ graniteConfig.web.commands.build
747
+ );
748
+ p4.log.success("\uB9C8\uC774\uADF8\uB808\uC774\uC158\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
749
+ }
750
+ async function getGraniteConfig(projectRoot) {
751
+ p4.log.info("granite.config \uD30C\uC77C \uCC3E\uB294 \uC911..");
752
+ const result = await cosmiconfig2("granite", {
753
+ loaders: {
754
+ ".ts": TypeScriptLoader2(),
755
+ ".cts": TypeScriptLoader2(),
756
+ ".mts": TypeScriptLoader2()
757
+ },
758
+ searchPlaces: [
759
+ "granite.config.ts",
760
+ "granite.config.cts",
761
+ "granite.config.mts",
762
+ "granite.config.js",
763
+ "granite.config.cjs",
764
+ "granite.config.mjs"
765
+ ]
766
+ }).search(projectRoot);
767
+ if (result == null) {
768
+ p4.log.error("granite.config \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
769
+ process.exit(1);
770
+ }
771
+ return result;
772
+ }
773
+ async function migrateAppsInTossConfig(configPath, outputPath) {
774
+ p4.log.info("granite.config\uB97C apps-in-toss.config\uB85C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4.");
775
+ const root = jscodeshift((await readFile3(configPath)).toString());
776
+ root.find(jscodeshift.ObjectExpression).forEach((path7) => {
777
+ const obj = path7.value;
778
+ for (const prop of obj.properties) {
779
+ if (jscodeshift.Property.check(prop) && jscodeshift.Identifier.check(prop.key) && prop.key.name === "brand" && jscodeshift.ObjectExpression.check(prop.value)) {
780
+ prop.value.properties = prop.value.properties.filter(
781
+ (prop2) => jscodeshift.Property.check(prop2) && jscodeshift.Identifier.check(prop2.key) && prop2.key.name === "primaryColor"
782
+ );
783
+ }
784
+ if (jscodeshift.Property.check(prop) && jscodeshift.Identifier.check(prop.key) && prop.key.name === "webViewProps") {
785
+ prop.key = jscodeshift.identifier("webView");
786
+ if (prop.value.type === "ObjectExpression") {
787
+ prop.value.properties = prop.value.properties.filter(
788
+ (prop2) => jscodeshift.Property.check(prop2) && jscodeshift.Identifier.check(prop2.key) && prop2.key.name !== "type"
789
+ );
790
+ }
791
+ }
792
+ if (jscodeshift.Property.check(prop) && jscodeshift.Identifier.check(prop.key) && prop.key.name === "outdir") {
793
+ prop.key = jscodeshift.identifier("webBundleDir");
794
+ }
795
+ }
796
+ obj.properties = obj.properties.filter((prop) => {
797
+ if (jscodeshift.Property.check(prop) && jscodeshift.Identifier.check(prop.key) && prop.key.name === "web" && jscodeshift.ObjectExpression.check(prop.value) && prop.value.properties.filter(
798
+ (prop2) => jscodeshift.Property.check(prop2) && jscodeshift.Identifier.check(prop2.key) && ["host", "port", "commands"].includes(prop2.key.name)
799
+ ).length > 0) {
800
+ return false;
801
+ }
802
+ return true;
803
+ });
804
+ });
805
+ await writeFile3(outputPath, root.toSource());
806
+ p4.log.info("apps-in-toss.config\uAC00 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
807
+ }
808
+ async function migratePackageJsonScripts(packageJsonPath, dev, build) {
809
+ p4.log.info("package.json\uC758 dev, build \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uC218\uC815\uD569\uB2C8\uB2E4.");
810
+ const packageJson = JSON.parse((await readFile3(packageJsonPath)).toString());
811
+ if (packageJson.scripts == null) {
812
+ packageJson.scripts = {};
813
+ }
814
+ packageJson.scripts["dev"] = dev;
815
+ packageJson.scripts["build"] = `${build} && ait build`;
816
+ await writeFile3(packageJsonPath, JSON.stringify(packageJson, null, 2));
817
+ p4.log.info("package.json \uC218\uC815\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
818
+ }
819
+
820
+ // src/cli/commands/migration/index.ts
821
+ var MIGRATION_TARGETS = {
822
+ v3: migrateWebFrameworkV3
823
+ };
824
+ var MigrationCommand = class extends Command4 {
825
+ static paths = [["migrate"]];
826
+ static usage = Command4.Usage({
827
+ category: "Migration",
828
+ description: "\uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC2E4\uD589\uD569\uB2C8\uB2E4.",
829
+ examples: [["Run migration", "apps-in-toss migrate <target>"]]
830
+ });
831
+ target = Option3.String({ required: true });
832
+ async execute() {
833
+ const target = MIGRATION_TARGETS[this.target];
834
+ if (target != null) {
835
+ await target();
836
+ } else {
837
+ p5.log.error(
838
+ [
839
+ "\uC798\uBABB\uB41C \uB9C8\uC774\uADF8\uB808\uC774\uC158 target \uC785\uB2C8\uB2E4. \uC544\uB798 \uC911 \uD558\uB098\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.\n",
840
+ ...Object.keys(MIGRATION_TARGETS)
841
+ ].join("\n")
842
+ );
843
+ process.exit(1);
844
+ }
845
+ }
846
+ };
847
+
848
+ // src/cli/commands/token/index.ts
849
+ import * as p6 from "@clack/prompts";
850
+ import { Command as Command5, Option as Option4 } from "clipanion";
851
+ var TokenCommand = class extends Command5 {
852
+ static paths = [["token"]];
853
+ static usage = Command5.Usage({
854
+ category: "Auth",
855
+ description: "\uD1A0\uD070 \uAD00\uB828 \uD558\uC704 \uBA85\uB839\uC744 \uAD00\uB9AC\uD569\uB2C8\uB2E4.",
856
+ examples: [
857
+ ["\uD1A0\uD070 \uCD94\uAC00", "apps-in-toss token add <profile?>"],
858
+ ["\uD1A0\uD070 \uC0AD\uC81C", "apps-in-toss token remove <profile?>"]
859
+ ]
860
+ });
861
+ async execute() {
862
+ }
863
+ };
864
+ var TokenAddCommand = class extends Command5 {
865
+ static paths = [["token", "add"]];
866
+ static usage = Command5.Usage({
867
+ category: "Auth",
868
+ description: "\uC2DC\uD06C\uB9BF \uD1A0\uD070\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4.",
869
+ examples: [
870
+ ["\uAE30\uBCF8 \uBCC4\uCE6D\uC73C\uB85C \uD1A0\uD070 \uCD94\uAC00", "apps-in-toss token add"],
871
+ ["\uBCC4\uCE6D\uC744 \uC9C0\uC815\uD558\uC5EC \uD1A0\uD070 \uCD94\uAC00", "apps-in-toss token add dev"]
872
+ ]
873
+ });
874
+ profile = Option4.String({ required: false });
875
+ apiKey = Option4.String("--api-key", { required: false });
876
+ async execute() {
877
+ const profile = this.profile || "default";
878
+ const secret = this.apiKey ? this.apiKey : await p6.password({
879
+ message: "Enter secret token:",
880
+ validate: (value) => {
881
+ if (value == null || value.length === 0) {
882
+ return "\uD1A0\uD070\uC740 \uBE44\uC5B4 \uC788\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
883
+ }
884
+ return;
885
+ }
886
+ });
887
+ if (p6.isCancel(secret)) {
888
+ return;
889
+ }
890
+ TokenStorage.set(profile, secret);
891
+ this.context.stdout.write(
892
+ `${profile} \uD504\uB85C\uD544\uB85C\uC758 \uC694\uCCAD\uC740 \uC774\uC81C \uBE44\uBC00 \uD1A0\uD070\uC744 \uC0AC\uC6A9\uD558\uC5EC \uC778\uC99D\uB429\uB2C8\uB2E4.
893
+ `
894
+ );
895
+ }
896
+ };
897
+ var TokenRemoveCommand = class extends Command5 {
898
+ static paths = [["token", "remove"]];
899
+ static usage = Command5.Usage({
900
+ category: "Auth",
901
+ description: "\uC2DC\uD06C\uB9BF \uD1A0\uD070\uC744 \uC0AD\uC81C\uD569\uB2C8\uB2E4.",
902
+ examples: [
903
+ ["\uAE30\uBCF8 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC758 \uD1A0\uD070 \uC0AD\uC81C", "apps-in-toss token remove"],
904
+ ["\uBCC4\uCE6D\uC744 \uC9C0\uC815\uD558\uC5EC \uD1A0\uD070 \uC0AD\uC81C", "apps-in-toss token remove dev"]
905
+ ]
906
+ });
907
+ profile = Option4.String({ required: false });
908
+ async execute() {
909
+ const profile = this.profile || "default";
910
+ const removed = TokenStorage.delete(profile);
911
+ if (removed) {
912
+ this.context.stdout.write(`\uD1A0\uD070\uC744 \uC81C\uAC70\uD588\uC2B5\uB2C8\uB2E4: ${profile}.
913
+ `);
914
+ } else {
915
+ this.context.stdout.write(`\uD1A0\uD070\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${profile}.
916
+ `);
917
+ }
918
+ }
919
+ };
920
+
921
+ // src/cli/index.ts
922
+ var cli = new Cli({
923
+ binaryLabel: "appsintoss",
924
+ binaryName: "appsintoss",
925
+ enableCapture: true
926
+ });
927
+ cli.register(BuildCommand);
928
+ cli.register(DeployCommand);
929
+ cli.register(InitCommand);
930
+ cli.register(MigrationCommand);
931
+ cli.register(TokenCommand);
932
+ cli.register(TokenAddCommand);
933
+ cli.register(TokenRemoveCommand);
934
+ cli.register(Builtins.HelpCommand);
935
+ cli.runExit(process.argv.slice(2));