@apps-in-toss/cli 3.0.3 → 3.0.4

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 (3) hide show
  1. package/LICENSE +201 -28
  2. package/dist/index.js +504 -47
  3. package/package.json +3 -2
package/LICENSE CHANGED
@@ -1,28 +1,201 @@
1
- BSD 3-Clause License
2
-
3
- Copyright (c) 2026, Viva Republica, Inc.
4
-
5
- Redistribution and use in source and binary forms, with or without
6
- modification, are permitted provided that the following conditions are met:
7
-
8
- 1. Redistributions of source code must retain the above copyright notice, this
9
- list of conditions and the following disclaimer.
10
-
11
- 2. Redistributions in binary form must reproduce the above copyright notice,
12
- this list of conditions and the following disclaimer in the documentation
13
- and/or other materials provided with the distribution.
14
-
15
- 3. Neither the name of the copyright holder nor the names of its
16
- contributors may be used to endorse or promote products derived from
17
- this software without specific prior written permission.
18
-
19
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Viva Republica, Inc.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/dist/index.js CHANGED
@@ -1,19 +1,260 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { Builtins, Cli, Command, Option } from "clipanion";
3
+ import { Builtins, Cli, Command, Option, formatMarkdownish } from "clipanion";
4
4
  import * as p from "@clack/prompts";
5
5
  import path from "path";
6
- import { appendFile, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
6
+ import { appendFile, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
7
  import path$1, { basename, dirname, extname, join, parse, resolve } from "node:path";
8
8
  import { AppsInTossBundle, PlatformType } from "@apps-in-toss/ait-format";
9
9
  import { loadConfig } from "c12";
10
10
  import { uuidv7 } from "uuidv7";
11
11
  import fs, { existsSync, readFileSync } from "node:fs";
12
12
  import assert from "node:assert";
13
+ import pc from "picocolors";
13
14
  import os from "node:os";
14
15
  import { setTimeout } from "node:timers/promises";
15
16
  import jscodeshift from "jscodeshift";
16
17
  import { spawn } from "node:child_process";
18
+ //#region src/i18n/en.ts
19
+ const en = {
20
+ "groupHelp.commandsHeader": "Commands",
21
+ "groupHelp.detailHint": "You can also print more details about any of these commands by calling them with the -h,--help flag right after the command name.",
22
+ "help.unknownPath": "Couldn't find the '{path}' command.",
23
+ "help.unknownPath.guide": "See the available commands below.",
24
+ "build.description": "Reads the config file (apps-in-toss.config) and builds a deployable .ait artifact.",
25
+ "build.details": `
26
+ Reads apps-in-toss.config.(ts|js|mjs) from the project root (the directory containing package.json) and builds a .ait artifact. This fails if that config file doesn't exist — create one first with \`ait init\` for a new project, or \`ait migrate v3\` for an existing granite.config project.
27
+
28
+ The directory set as webBundleDir in apps-in-toss.config is packed into the artifact as-is, so before running this command you need to run your web framework's build command (e.g. vite build) so that directory has build output that includes index.html.
29
+
30
+ The finished artifact is saved as \`<appName>.ait\` in the project root, and \`ait deploy\` finds and uploads this file.
31
+ `,
32
+ "build.example.artifact.label": "Build artifact",
33
+ "deploy.description": "Uploads (deploys) a built .ait artifact to Apps in Toss.",
34
+ "deploy.details": `
35
+ The API key is resolved in this order:
36
+
37
+ - If a token is saved for the profile named by \`--profile\` (or \`default\` when not given), that token is used.
38
+ - If no saved token exists, the value passed directly via \`--api-key\` is used.
39
+ - If neither is available, you're prompted interactively for the API key.
40
+
41
+ To save a token for a profile ahead of time, register it first with \`ait token add\`.
42
+ `,
43
+ "deploy.example.default.label": "Deploy the default artifact (.ait)",
44
+ "deploy.example.location.label": "Deploy a specific .ait file",
45
+ "deploy.example.profile.label": "Deploy using a token saved in a profile",
46
+ "deploy.example.memo.label": "Deploy with release notes",
47
+ "deploy.example.memo.cmd": "ait deploy -m \"Release notes (up to 1000 chars)\"",
48
+ "deploy.example.schemeOnly.label": "Print only the deployment result scheme",
49
+ "deploy.option.apiKey": "The API key to use for deployment. Either pass it directly with this option, or use --profile to specify a profile and use the API key registered for it.",
50
+ "deploy.option.workspace": "(deprecated) The token workspace name. Use --profile instead.",
51
+ "deploy.option.profile": "A profile registered in ~/.ait/credentials via the ait token add command. Falls back to the default profile if none is given.",
52
+ "deploy.option.baseUrl": "Directly overrides the base URL of the deploy API. Not needed for a normal deploy — only used when testing against a separate environment.",
53
+ "deploy.option.location": "Explicitly sets the path to the .ait file to upload. If omitted, uses the first .ait file found in the current package root directory.",
54
+ "deploy.option.schemeOnly": "Makes the deploy print only the intoss-private scheme for the result. Defaults to false.",
55
+ "deploy.option.memo": "Sets the release notes. Up to 1000 characters.",
56
+ "init.description": "Adds the settings needed to deploy to Apps in Toss to an existing web project.",
57
+ "init.details": `
58
+ Doesn't create a new project — run this inside an existing web project that has a package.json (e.g. one created with Vite).
59
+
60
+ Running it does the following:
61
+
62
+ - Asks for an app name (kebab-case) and the web bundle output directory (default: dist).
63
+ - Creates an apps-in-toss.config.ts file.
64
+ - If package.json has a build script, appends \`&& ait build\` to it, and adds a \`deploy\` (\`ait deploy\`) script.
65
+ - Adds @apps-in-toss/devtools to devDependencies, and finds your vite/webpack/rspack/rollup config file to automatically plug the devtools plugin into its plugins array (shows manual setup instructions if it can't be plugged in automatically).
66
+ - Adds \`*.ait\` to .gitignore, and installs dependencies with the detected package manager.
67
+ `,
68
+ "init.example.interactive.label": "Initialize interactively",
69
+ "init.example.appName.label": "Initialize with an app name",
70
+ "init.option.appName": "Sets the app name in kebab-case. If omitted, you're prompted interactively. e.g. my-miniapp",
71
+ "migration.description": "Automatically converts an @apps-in-toss/web-framework 2.x project to the 3.x config format.",
72
+ "migration.details": `
73
+ Selects which migration to run via the target argument. Currently only one target is registered: \`v3\`.
74
+
75
+ \`ait migrate v3\` converts granite.config.(ts|js|mjs) to apps-in-toss.config with the same extension.
76
+
77
+ - Removes every property under \`brand\` except \`primaryColor\`.
78
+ - Renames \`webViewProps\` to \`webView\`, and removes the \`type\` property inside it.
79
+ - Renames \`outdir\` to \`webBundleDir\`.
80
+ - Moves the \`web.commands.dev\`/\`web.commands.build\` values to the \`dev\`/\`build\` scripts in \`package.json\`, and appends \`&& ait build\` to the end of the \`build\` script.
81
+ - Adds \`@apps-in-toss/devtools\` to \`devDependencies\`, and finds your vite/webpack/rspack/rollup config file to automatically plug the devtools plugin into its plugins array (shows manual setup instructions if it can't be plugged in automatically).
82
+ - Once all of the above changes are done, deletes the original granite.config file and runs install with the detected package manager.
83
+
84
+ If granite.config can't be found, or web.commands.dev/build isn't a string, nothing is changed and the reason is reported. If the conversion fails partway through, the changes applied so far are automatically rolled back — but once it finishes successfully it can't be rolled back, so we recommend saving your current state (e.g. with a git commit) before running it.
85
+ `,
86
+ "migration.example.v3.label": "Migrate to web-framework 3.x",
87
+ "token.description": "Manages API tokens used for deployment, per profile.",
88
+ "token.details": `
89
+ A profile is an API token saved under a name in the ~/.ait/credentials file. Specify \`ait deploy --profile <name>\` to deploy using the token saved for that profile; omit --profile to use the default profile.
90
+ `,
91
+ "token.example.add.label": "Add a token",
92
+ "token.example.remove.label": "Remove a token",
93
+ "tokenAdd.description": "Adds a secret token. Tokens are stored per profile in ~/.ait/credentials, and can be loaded with ait deploy --profile.",
94
+ "tokenAdd.example.default.label": "Add a token under the default profile",
95
+ "tokenAdd.example.profile.label": "Add a token under a named profile",
96
+ "tokenAdd.example.apiKey.label": "Pass the API key directly as an option",
97
+ "tokenAdd.option.apiKey": "Sets the token value directly. If omitted, you're prompted interactively.",
98
+ "tokenRemove.description": "Removes a secret token stored in ~/.ait/credentials.",
99
+ "tokenRemove.example.default.label": "Remove the default profile's token",
100
+ "tokenRemove.example.profile.label": "Remove a token by profile name"
101
+ };
102
+ //#endregion
103
+ //#region src/i18n/ko.ts
104
+ const ko = {
105
+ "groupHelp.commandsHeader": "Commands",
106
+ "groupHelp.detailHint": "각 명령어 뒤에 -h 또는 --help 옵션을 붙이면 더 자세한 설명을 볼 수 있어요.",
107
+ "help.unknownPath": "'{path}' 명령어를 찾을 수 없어요.",
108
+ "help.unknownPath.guide": "사용 가능한 명령어는 아래에서 확인해주세요.",
109
+ "build.description": "설정 파일(apps-in-toss.config)을 읽어 배포용 .ait 아티팩트를 만들어요.",
110
+ "build.details": `
111
+ 프로젝트 루트(package.json이 있는 디렉토리)의 apps-in-toss.config.(ts|js|mjs)를 읽어서 .ait 아티팩트를 만들어요. 이 설정 파일이 없으면 실행에 실패해요 — 신규 프로젝트라면 \`ait init\`으로, 기존 granite.config 프로젝트라면 \`ait migrate v3\`로 먼저 만들어주세요.
112
+
113
+ apps-in-toss.config의 webBundleDir에 지정한 디렉토리를 그대로 아티팩트에 담기 때문에, 이 명령을 실행하기 전에 웹 프레임워크의 빌드 명령(예: vite build)을 먼저 실행해서 그 디렉토리에 index.html을 포함한 산출물이 있어야 해요.
114
+
115
+ 완성된 아티팩트는 프로젝트 루트에 \`<appName>.ait\`로 저장되고, \`ait deploy\`가 이 파일을 찾아 업로드해요.
116
+ `,
117
+ "build.example.artifact.label": "아티팩트 빌드",
118
+ "deploy.description": "빌드된 .ait 아티팩트를 앱인토스에 업로드(배포)해요.",
119
+ "deploy.details": `
120
+ API 키는 다음 순서로 결정돼요.
121
+
122
+ - \`--profile\`로 지정한(또는 지정하지 않았을 때 \`default\`) 프로필에 저장된 토큰이 있으면 그걸 사용해요.
123
+ - 저장된 토큰이 없으면 \`--api-key\`로 직접 입력한 값을 사용해요.
124
+ - 둘 다 없으면 대화형 프롬프트로 API 키를 입력받아요.
125
+
126
+ 프로필에 토큰을 저장해두고 싶다면 \`ait token add\`로 먼저 등록해주세요.
127
+ `,
128
+ "deploy.example.default.label": "기본 아티팩트(.ait) 배포",
129
+ "deploy.example.location.label": "특정 .ait 파일 배포",
130
+ "deploy.example.profile.label": "프로필에 저장된 토큰으로 배포",
131
+ "deploy.example.memo.label": "출시 메모와 함께 배포",
132
+ "deploy.example.memo.cmd": "ait deploy -m \"출시 메모(최대 1000자)\"",
133
+ "deploy.example.schemeOnly.label": "배포 결과 scheme만 출력",
134
+ "deploy.option.apiKey": "배포를 위한 API 키예요. 이 옵션으로 직접 입력하거나, --profile 옵션으로 프로필을 지정하면 그 프로필에 등록된 API 키를 사용해요.",
135
+ "deploy.option.workspace": "(deprecated) 토큰 워크스페이스 이름이에요. 이 옵션 대신 --profile 옵션을 사용해주세요.",
136
+ "deploy.option.profile": "ait token add 명령어로 ~/.ait/credentials에 등록한 프로필이에요. 프로필이 없으면 default 프로필을 사용해요.",
137
+ "deploy.option.baseUrl": "배포 API의 base URL을 직접 지정해요. 일반적인 배포에서는 지정할 필요가 없고, 별도 환경을 대상으로 테스트할 때만 사용해요.",
138
+ "deploy.option.location": "업로드할 .ait 파일의 경로를 명시적으로 지정해요. 지정하지 않으면 현재 패키지 루트 디렉토리에서 발견되는 첫 .ait 파일을 사용해요.",
139
+ "deploy.option.schemeOnly": "배포 결과를 intoss-private scheme만 출력하도록 설정해요. 기본값은 false예요.",
140
+ "deploy.option.memo": "출시 메모를 입력해요. 최대 1000자까지 입력할 수 있어요.",
141
+ "init.description": "기존 웹 프로젝트에 앱인토스 배포에 필요한 설정을 추가해요.",
142
+ "init.details": `
143
+ 새 프로젝트를 만들지는 않아요 — package.json이 있는 기존 웹 프로젝트(예: Vite로 만든 프로젝트)에서 실행해주세요.
144
+
145
+ 실행하면 다음을 처리해요.
146
+
147
+ - 앱 이름(케밥-케이스)을 물어보고, 웹 번들 결과물 디렉토리를 물어봐요(기본값 dist).
148
+ - apps-in-toss.config.ts 파일을 생성해요.
149
+ - package.json에 build 스크립트가 있으면 뒤에 \`&& ait build\`를 붙이고, \`deploy\`(\`ait deploy\`) 스크립트를 추가해요.
150
+ - devDependencies에 @apps-in-toss/devtools를 추가하고, vite/webpack/rspack/rollup 설정 파일을 찾아 plugins 배열에 devtools 플러그인을 자동으로 꽂아요(자동으로 못 꽂으면 수동 설치 안내를 보여줘요).
151
+ - .gitignore에 \`*.ait\`를 추가하고, 감지한 패키지 매니저로 의존성을 설치해요.
152
+ `,
153
+ "init.example.interactive.label": "대화형으로 초기화",
154
+ "init.example.appName.label": "앱 이름을 지정하여 초기화",
155
+ "init.option.appName": "앱 이름을 케밥-케이스로 지정해요. 생략하면 대화형 프롬프트로 입력받아요. 예) my-miniapp",
156
+ "migration.description": "@apps-in-toss/web-framework 2.x 프로젝트를 3.x 설정 형식으로 자동 변환해요.",
157
+ "migration.details": `
158
+ target 인자로 실행할 마이그레이션을 선택해요. 현재 등록된 타깃은 \`v3\` 하나예요.
159
+
160
+ \`ait migrate v3\`는 granite.config.(ts|js|mjs)를 같은 확장자의 apps-in-toss.config로 변환해요.
161
+
162
+ - \`brand\`에서 \`primaryColor\`를 제외한 나머지 속성은 지워요.
163
+ - \`webViewProps\`는 \`webView\`로 이름을 바꾸고, 그 안의 \`type\` 속성은 지워요.
164
+ - \`outdir\`은 \`webBundleDir\`로 이름을 바꿔요.
165
+ - \`web.commands.dev\`/\`web.commands.build\` 값을 \`package.json\`의 \`dev\`/\`build\` 스크립트로 옮기고, \`build\` 스크립트 끝에 \`&& ait build\`를 붙여요.
166
+ - \`devDependencies\`에 \`@apps-in-toss/devtools\`를 추가하고, vite/webpack/rspack/rollup 설정 파일을 찾아 plugins 배열에 devtools 플러그인을 자동으로 꽂아요(자동으로 못 꽂으면 수동 설치 안내를 보여줘요).
167
+ - 위 변경이 모두 끝나면 원본 granite.config 파일을 삭제하고, 감지한 패키지 매니저로 설치를 실행해요.
168
+
169
+ granite.config를 찾지 못했거나 web.commands.dev/build가 문자열로 없으면 아무것도 바꾸지 않고 원인을 알려줘요. 변환 도중 실패하면 그때까지 적용한 변경은 자동으로 되돌리지만, 성공적으로 끝난 뒤에는 되돌릴 수 없으니 실행 전에 git commit 등으로 현재 상태를 저장해두는 걸 권장해요.
170
+ `,
171
+ "migration.example.v3.label": "web-framework 3.x로 마이그레이션",
172
+ "token.description": "배포에 사용할 API 토큰을 프로필별로 관리해요.",
173
+ "token.details": `
174
+ 프로필은 ~/.ait/credentials 파일에 이름으로 구분해서 저장하는 API 토큰이에요. \`ait deploy --profile <이름>\`으로 지정하면 그 프로필에 저장된 토큰을 사용해서 배포하고, --profile을 생략하면 default 프로필을 사용해요.
175
+ `,
176
+ "token.example.add.label": "토큰 추가",
177
+ "token.example.remove.label": "토큰 삭제",
178
+ "tokenAdd.description": "시크릿 토큰을 추가해요. 토큰은 ~/.ait/credentials에 프로필별로 저장되고, ait deploy --profile로 불러올 수 있어요.",
179
+ "tokenAdd.example.default.label": "기본 프로필로 토큰 추가",
180
+ "tokenAdd.example.profile.label": "프로필을 지정하여 토큰 추가",
181
+ "tokenAdd.example.apiKey.label": "API 키를 옵션으로 직접 전달",
182
+ "tokenAdd.option.apiKey": "토큰 값을 직접 지정해요. 생략하면 대화형 프롬프트로 입력받아요.",
183
+ "tokenRemove.description": "~/.ait/credentials에 저장된 시크릿 토큰을 삭제해요.",
184
+ "tokenRemove.example.default.label": "기본 프로필의 토큰 삭제",
185
+ "tokenRemove.example.profile.label": "프로필을 지정하여 토큰 삭제"
186
+ };
187
+ //#endregion
188
+ //#region src/i18n/index.ts
189
+ /**
190
+ * i18n for the CLI's --help/usage layer only (command `description`/
191
+ * `details`/`examples`, `Option(...)` `description`). Runtime messages
192
+ * (errors, interactive prompts, progress output, migration logs) are NOT
193
+ * covered by this module and keep using their existing Korean strings
194
+ * directly in the command files.
195
+ *
196
+ * Locale is resolved ONCE per process, at module load time, from
197
+ * `process.env` — not re-detected per call — because clipanion's
198
+ * `Command.Usage()` bakes its strings into static class fields at class
199
+ * definition time, so the active locale has to be known before any command
200
+ * module is evaluated.
201
+ *
202
+ * Locale decision policy (maintainer-confirmed):
203
+ *
204
+ * 1. `AIT_LANG` env var, trimmed and lowercased. If it's `"ko"` or `"en"`,
205
+ * that value wins outright. Any other value is ignored and falls
206
+ * through to step 2.
207
+ * 2. Otherwise, check `LC_ALL` → `LC_CTYPE` → `LANG`, in that order, for
208
+ * the first non-empty value (an empty string counts as unset, per
209
+ * POSIX convention). If that value's language tag matches `ko`
210
+ * (`ko`, `ko_KR.UTF-8`, `ko-KR`, ... — anything starting with `ko`
211
+ * followed by `.`/`_`/`-`/end-of-string), the locale is `ko`.
212
+ * Everything else (`en_US.UTF-8`, `C`, `POSIX`, ...) resolves to `en`.
213
+ * 3. If all three are empty/unset, the locale is `en`.
214
+ */
215
+ const KO_LANGUAGE_TAG_RE = /^ko([._-]|$)/i;
216
+ /**
217
+ * Pure decision function — takes `process.env` as an input instead of
218
+ * reading it directly, so the policy above is unit-testable without
219
+ * mutating global process state.
220
+ */
221
+ function resolveLocale(env) {
222
+ const override = env.AIT_LANG?.trim().toLowerCase();
223
+ if (override === "ko" || override === "en") return override;
224
+ const firstNonEmpty = [
225
+ env.LC_ALL,
226
+ env.LC_CTYPE,
227
+ env.LANG
228
+ ].find((value) => value != null && value !== "");
229
+ if (firstNonEmpty != null && KO_LANGUAGE_TAG_RE.test(firstNonEmpty)) return "ko";
230
+ return "en";
231
+ }
232
+ /** The active locale for this process, decided once at module load. */
233
+ const locale = resolveLocale(process.env);
234
+ const tables = {
235
+ ko,
236
+ en
237
+ };
238
+ /**
239
+ * Look up a help/usage string for the active locale, with `{name}`
240
+ * placeholder interpolation. Falls back to the key itself if missing, so a
241
+ * forgotten key surfaces visibly rather than rendering empty.
242
+ */
243
+ function t(key, vars) {
244
+ const raw = tables[locale][key] ?? key;
245
+ if (!vars) return raw;
246
+ return raw.replace(/\{(\w+)\}/g, (match, name) => {
247
+ const value = vars[name];
248
+ return value === void 0 ? match : String(value);
249
+ });
250
+ }
251
+ const HINT_WHEN_ACTIVE_IS_KO = "Set AIT_LANG=en for help in English.";
252
+ const HINT_WHEN_ACTIVE_IS_EN = "AIT_LANG=ko를 설정하면 한국어로 볼 수 있어요.";
253
+ /** The dim cross-locale discovery hint appended to top-level help output. */
254
+ function helpHint() {
255
+ return locale === "ko" ? HINT_WHEN_ACTIVE_IS_KO : HINT_WHEN_ACTIVE_IS_EN;
256
+ }
257
+ //#endregion
17
258
  //#region src/utils/getPackageRoot.ts
18
259
  function getPackageRoot(base) {
19
260
  let dirPath = base;
@@ -49,7 +290,12 @@ async function buildArtifact() {
49
290
  }
50
291
  });
51
292
  appsInTossConfig.permissions.forEach(({ name, access }) => writer.addPermission(name, access));
52
- const webBundleFiles = await getWebBundleFiles(path$1.resolve(projectRoot, appsInTossConfig.webBundleDir));
293
+ const webBundleDir = path$1.resolve(projectRoot, appsInTossConfig.webBundleDir);
294
+ if ((await stat(webBundleDir).catch(() => null))?.isDirectory() !== true) {
295
+ p.log.error(`웹 빌드 디렉토리(${appsInTossConfig.webBundleDir})가 존재하지 않습니다. 웹 빌드를 먼저 실행해주세요.`);
296
+ process.exit(1);
297
+ }
298
+ const webBundleFiles = await getWebBundleFiles(webBundleDir);
53
299
  if (webBundleFiles.find((file) => file.name === "index.html") == null) {
54
300
  p.log.error("웹 빌드 디렉토리에 index.html이 존재해야 합니다.");
55
301
  process.exit(1);
@@ -95,7 +341,23 @@ async function getAppsInTossConfig() {
95
341
  packageJson: false
96
342
  });
97
343
  if (configFile == null) throw new Error("Config 파일을 찾을 수 없습니다.");
98
- return config;
344
+ if (typeof config.appName !== "string" || config.appName.trim() === "") {
345
+ p.log.error("apps-in-toss.config에 appName이 설정되어야 합니다. (예: appName: 'my-app')");
346
+ process.exit(1);
347
+ }
348
+ if (typeof config.brand?.primaryColor !== "string" || config.brand.primaryColor.trim() === "") {
349
+ p.log.error("apps-in-toss.config에 brand.primaryColor가 설정되어야 합니다. (예: brand: { primaryColor: '#3182F6' })");
350
+ process.exit(1);
351
+ }
352
+ const webBundleDir = config.webBundleDir ?? "dist";
353
+ if (typeof webBundleDir !== "string" || webBundleDir.trim() === "") {
354
+ p.log.error("apps-in-toss.config의 webBundleDir이 올바르지 않습니다.");
355
+ process.exit(1);
356
+ }
357
+ return {
358
+ ...config,
359
+ webBundleDir
360
+ };
99
361
  }
100
362
  function getUnityMetadata() {
101
363
  const unityMetadataString = process.env.UNITY_METADATA;
@@ -133,8 +395,9 @@ var BuildCommand = class extends Command {
133
395
  static paths = [["build"]];
134
396
  static usage = Command.Usage({
135
397
  category: "Build",
136
- description: "앱인토스 배포용 .ait 아티팩트를 빌드합니다.",
137
- examples: [["Build artifact", "apps-in-toss build"]]
398
+ description: t("build.description"),
399
+ details: t("build.details"),
400
+ examples: [[t("build.example.artifact.label"), "ait build"]]
138
401
  });
139
402
  async execute() {
140
403
  const buildResult = await buildArtifact();
@@ -144,14 +407,13 @@ var BuildCommand = class extends Command {
144
407
  };
145
408
  //#endregion
146
409
  //#region src/utils/colors.ts
147
- function wrap(open, close) {
148
- return (input) => `\x1b[${open}m${input}\x1b[${close}m`;
149
- }
150
410
  const colors = {
151
- cyan: wrap(36, 39),
152
- green: wrap(32, 39),
153
- underline: wrap(4, 24)
411
+ cyan: pc.cyan,
412
+ dim: pc.dim,
413
+ green: pc.green,
414
+ underline: pc.underline
154
415
  };
416
+ const isColorSupported = pc.isColorSupported;
155
417
  //#endregion
156
418
  //#region src/utils/TokenStorage.ts
157
419
  var TokenStorage = class TokenStorage {
@@ -322,42 +584,43 @@ var DeployCommand = class extends Command {
322
584
  static paths = [["deploy"]];
323
585
  static usage = Command.Usage({
324
586
  category: "Deploy",
325
- description: "빌드된 .ait 아티팩트를 앱인토스에 업로드(배포)합니다.",
587
+ description: t("deploy.description"),
588
+ details: t("deploy.details"),
326
589
  examples: [
327
- ["기본 아티팩트(.ait) 배포", "apps-in-toss deploy"],
328
- ["특정 .ait 파일 배포", "apps-in-toss deploy --location ./dist/my-app.ait"],
329
- ["프로필에 저장된 토큰으로 배포", "apps-in-toss deploy --profile dev"],
330
- ["출시 메모와 함께 배포", "apps-in-toss deploy -m \"출시 메모(최대 1000자)\""],
331
- ["배포 결과 scheme만 출력", "apps-in-toss deploy --scheme-only"]
590
+ [t("deploy.example.default.label"), "ait deploy"],
591
+ [t("deploy.example.location.label"), "ait deploy --location ./dist/my-app.ait"],
592
+ [t("deploy.example.profile.label"), "ait deploy --profile dev"],
593
+ [t("deploy.example.memo.label"), t("deploy.example.memo.cmd")],
594
+ [t("deploy.example.schemeOnly.label"), "ait deploy --scheme-only"]
332
595
  ]
333
596
  });
334
597
  apiKey = Option.String("--api-key", {
335
598
  required: false,
336
- description: "배포를 위한 API 키입니다. 명시적으로 이 옵션으로 api key를 직접 입력하거나, --profile 옵션을 사용해서 프로필을 지정하면 프로필에 등록된 api key를 사용해요."
599
+ description: t("deploy.option.apiKey")
337
600
  });
338
601
  workspace = Option.String("--workspace", {
339
602
  required: false,
340
- description: "(deprecated) 토큰 워크스페이스 이름입니다. 이 옵션 대신 --profile 옵션을 사용해주세요."
603
+ description: t("deploy.option.workspace")
341
604
  });
342
605
  profile = Option.String("--profile", {
343
606
  required: false,
344
- description: "apps-in-toss token add 명령어를 통해 등록한 프로필이에요. 프로필이 없으면 default 프로필을 사용해요."
607
+ description: t("deploy.option.profile")
345
608
  });
346
609
  baseUrl = Option.String("--base-url", {
347
- description: "API Base URL",
610
+ description: t("deploy.option.baseUrl"),
348
611
  required: false
349
612
  });
350
613
  location = Option.String("--location", {
351
- description: "업로드할 .ait 파일의 경로를 명시적으로 지정해요. 기본값은 현재 패키지 루트 디렉토리에 있는 ait 파일이에요.",
614
+ description: t("deploy.option.location"),
352
615
  required: false
353
616
  });
354
617
  schemeOnly = Option.Boolean("--scheme-only", {
355
618
  required: false,
356
- description: "배포 결과를 intoss-private scheme만 출력하도록 설정해요. 기본값은 false예요."
619
+ description: t("deploy.option.schemeOnly")
357
620
  });
358
621
  memo = Option.String("-m,--memo", {
359
622
  required: false,
360
- description: "출시 메모를 입력해요. 최대 1000자까지 입력할 수 있어요."
623
+ description: t("deploy.option.memo")
361
624
  });
362
625
  async execute() {
363
626
  const baseUrl = this.baseUrl;
@@ -410,7 +673,7 @@ var DeployCommand = class extends Command {
410
673
  return colors.underline(color(message));
411
674
  }
412
675
  printResult(appName, deploymentId) {
413
- const result = `intoss-private://${appName}?_deploymentId=${deploymentId}`;
676
+ const result = `intoss-private://${appName}?_deploymentId=${deploymentId}&host=appsInTossHost`;
414
677
  if (this.schemeOnly) this.context.stdout.write(`${result}\n`);
415
678
  else p.note(this.decorate(result, colors.green));
416
679
  }
@@ -764,12 +1027,13 @@ var InitCommand = class extends Command {
764
1027
  static paths = [["init"]];
765
1028
  static usage = Command.Usage({
766
1029
  category: "Setup",
767
- description: "앱인토스 웹 프레임워크 개발/배포 설정을 초기화합니다.",
768
- examples: [["대화형으로 초기화", "apps-in-toss init"], ["앱 이름을 지정하여 초기화", "apps-in-toss init --app-name my-miniapp"]]
1030
+ description: t("init.description"),
1031
+ details: t("init.details"),
1032
+ examples: [[t("init.example.interactive.label"), "ait init"], [t("init.example.appName.label"), "ait init --app-name my-miniapp"]]
769
1033
  });
770
1034
  appName = Option.String("--app-name", {
771
1035
  required: false,
772
- description: "앱 이름(케밥-케이스)을 지정해요. 예) my-miniapp"
1036
+ description: t("init.option.appName")
773
1037
  });
774
1038
  skipInput = Option.Boolean("--skip-input", {
775
1039
  required: false,
@@ -1108,8 +1372,9 @@ var MigrationCommand = class extends Command {
1108
1372
  static paths = [["migrate"]];
1109
1373
  static usage = Command.Usage({
1110
1374
  category: "Migration",
1111
- description: "마이그레이션을 실행해요.",
1112
- examples: [["Run migration", "apps-in-toss migrate <target>"]]
1375
+ description: t("migration.description"),
1376
+ details: t("migration.details"),
1377
+ examples: [[t("migration.example.v3.label"), "ait migrate v3"]]
1113
1378
  });
1114
1379
  target = Option.String({ required: true });
1115
1380
  async execute() {
@@ -1122,25 +1387,96 @@ var MigrationCommand = class extends Command {
1122
1387
  }
1123
1388
  };
1124
1389
  //#endregion
1390
+ //#region src/utils/groupHelp.ts
1391
+ /**
1392
+ * Renders a styled "group help" page for a Command whose only job is to
1393
+ * gather related subcommands under a shared path prefix (e.g. `ait token`
1394
+ * grouping `token add`/`token remove`).
1395
+ *
1396
+ * Clipanion has no built-in notion of a "subcommand group": a bare group
1397
+ * invocation just runs whatever `execute()` the group's own Command defines,
1398
+ * and `<group> --help` normally resolves to clipanion's *ambiguous
1399
+ * multi-match* help path (every registered command whose path starts with
1400
+ * the given prefix) rather than anything group-shaped. This helper is meant
1401
+ * to be called from that group Command's own `execute()` instead.
1402
+ *
1403
+ * It leans on clipanion's own rendering primitives — `cli.usage()` for each
1404
+ * subcommand's usage line, `formatMarkdownish` for description text, and
1405
+ * `cli.format()` for header/bold styling — so the output stays visually
1406
+ * consistent with clipanion's native `--help` pages instead of hand-rolling
1407
+ * ANSI output. `cli.format()`/`cli.usage()` already resolve color support
1408
+ * the same way the rest of the CLI's help output does, so callers don't need
1409
+ * to thread a `colored` flag through.
1410
+ *
1411
+ * The result always ends with the dim cross-locale discovery hint (see
1412
+ * `helpHint()`), so every caller of this helper — the group's own bare
1413
+ * invocation (e.g. `ait token`) and the `help` router (`ait help token` /
1414
+ * `ait token --help`, rewritten to the same thing by `rewriteHelpArgv`) —
1415
+ * automatically renders it without having to append it themselves.
1416
+ */
1417
+ function renderGroupHelp(cli, groupUsage, subcommands, labels) {
1418
+ const format = cli.format();
1419
+ let result = ``;
1420
+ if (groupUsage.description) {
1421
+ result += formatMarkdownish(groupUsage.description, {
1422
+ format,
1423
+ paragraphs: false
1424
+ }).replace(/^./, (char) => char.toUpperCase());
1425
+ result += `\n`;
1426
+ }
1427
+ result += `\n`;
1428
+ result += `${format.header(labels.commandsHeader)}\n`;
1429
+ for (const subcommand of subcommands) {
1430
+ result += `\n`;
1431
+ result += cli.usage(subcommand);
1432
+ const description = subcommand.usage?.description;
1433
+ if (description) result += ` ${formatMarkdownish(description, {
1434
+ format,
1435
+ paragraphs: false
1436
+ })}`;
1437
+ }
1438
+ result += `\n`;
1439
+ result += formatMarkdownish(labels.detailHint, {
1440
+ format,
1441
+ paragraphs: true
1442
+ });
1443
+ result += `${colors.dim(helpHint())}\n`;
1444
+ return result;
1445
+ }
1446
+ //#endregion
1125
1447
  //#region src/commands/token/index.ts
1448
+ const tokenUsage = Command.Usage({
1449
+ category: "Auth",
1450
+ description: t("token.description"),
1451
+ details: t("token.details"),
1452
+ examples: [[t("token.example.add.label"), "ait token add <profile?>"], [t("token.example.remove.label"), "ait token remove <profile?>"]]
1453
+ });
1126
1454
  var TokenCommand = class extends Command {
1127
1455
  static paths = [["token"]];
1128
- static usage = Command.Usage({
1129
- category: "Auth",
1130
- description: "토큰 관련 하위 명령을 관리합니다.",
1131
- examples: [["토큰 추가", "apps-in-toss token add <profile?>"], ["토큰 삭제", "apps-in-toss token remove <profile?>"]]
1132
- });
1133
- async execute() {}
1456
+ static usage = tokenUsage;
1457
+ async execute() {
1458
+ this.context.stdout.write(renderGroupHelp(this.cli, tokenUsage, [TokenAddCommand, TokenRemoveCommand], {
1459
+ commandsHeader: t("groupHelp.commandsHeader"),
1460
+ detailHint: t("groupHelp.detailHint")
1461
+ }));
1462
+ }
1134
1463
  };
1135
1464
  var TokenAddCommand = class extends Command {
1136
1465
  static paths = [["token", "add"]];
1137
1466
  static usage = Command.Usage({
1138
1467
  category: "Auth",
1139
- description: "시크릿 토큰을 추가합니다.",
1140
- examples: [["기본 별칭으로 토큰 추가", "apps-in-toss token add"], ["별칭을 지정하여 토큰 추가", "apps-in-toss token add dev"]]
1468
+ description: t("tokenAdd.description"),
1469
+ examples: [
1470
+ [t("tokenAdd.example.default.label"), "ait token add"],
1471
+ [t("tokenAdd.example.profile.label"), "ait token add dev"],
1472
+ [t("tokenAdd.example.apiKey.label"), "ait token add dev --api-key <API_KEY>"]
1473
+ ]
1141
1474
  });
1142
1475
  profile = Option.String({ required: false });
1143
- apiKey = Option.String("--api-key", { required: false });
1476
+ apiKey = Option.String("--api-key", {
1477
+ required: false,
1478
+ description: t("tokenAdd.option.apiKey")
1479
+ });
1144
1480
  async execute() {
1145
1481
  const profile = this.profile || "default";
1146
1482
  const secret = this.apiKey ? this.apiKey : await p.password({
@@ -1158,8 +1494,8 @@ var TokenRemoveCommand = class extends Command {
1158
1494
  static paths = [["token", "remove"]];
1159
1495
  static usage = Command.Usage({
1160
1496
  category: "Auth",
1161
- description: "시크릿 토큰을 삭제합니다.",
1162
- examples: [["기본 워크스페이스의 토큰 삭제", "apps-in-toss token remove"], ["별칭을 지정하여 토큰 삭제", "apps-in-toss token remove dev"]]
1497
+ description: t("tokenRemove.description"),
1498
+ examples: [[t("tokenRemove.example.default.label"), "ait token remove"], [t("tokenRemove.example.profile.label"), "ait token remove dev"]]
1163
1499
  });
1164
1500
  profile = Option.String({ required: false });
1165
1501
  async execute() {
@@ -1169,11 +1505,129 @@ var TokenRemoveCommand = class extends Command {
1169
1505
  }
1170
1506
  };
1171
1507
  //#endregion
1508
+ //#region src/utils/argv.ts
1509
+ /**
1510
+ * Rewrites any invocation that carries a bare `--help`/`-h` token into
1511
+ * `["help", ...path]`, where `path` is the leading run of non-flag tokens
1512
+ * that precede it (stopping at the first token that looks like a flag,
1513
+ * i.e. starts with `-`).
1514
+ *
1515
+ * This lets a single registered command (`MainHelpCommand`, see
1516
+ * `src/index.ts`) own every help surface instead of relying on clipanion's
1517
+ * per-path internal help machinery, which:
1518
+ *
1519
+ * - never instantiates a Command for `<command> --help` (it prints usage
1520
+ * directly from inside `Cli#run`, see `Command#help` in clipanion's
1521
+ * `advanced/Command.d.ts`), so that path can't be hooked to append the
1522
+ * cross-locale hint line (see `helpHint()` in `../i18n`).
1523
+ * - resolves `<group> --help` (e.g. `ait token --help`) through its
1524
+ * *ambiguous multi-match* help path whenever the prefix matches more
1525
+ * than one registered command (`token`, `token add`, `token remove`),
1526
+ * printing a generic "Multiple commands match your selection" listing
1527
+ * instead of anything specific to the group.
1528
+ *
1529
+ * Examples:
1530
+ * `["deploy", "--help"]` → `["help", "deploy"]`
1531
+ * `["token", "add", "-h"]` → `["help", "token", "add"]`
1532
+ * `["--help"]` → `["help"]`
1533
+ * `["token", "--help"]` → `["help", "token"]`
1534
+ * `["deploy", "--profile", "x", "--help"]` → `["help", "deploy"]`
1535
+ * (the path stops at the first flag-like token, `--profile`, even
1536
+ * though that's not the `--help` token itself — everything from there
1537
+ * on is treated as options for whatever command the path names)
1538
+ *
1539
+ * `-h=<index>` (an explicit index into clipanion's ambiguous-match listing
1540
+ * — e.g. `ait token -h=1`) is intentionally left untouched: it never
1541
+ * matches the exact-token check below (it's the single string `"-h=1"`,
1542
+ * not `"-h"`), so it keeps flowing through clipanion's original internal
1543
+ * help machinery unchanged.
1544
+ *
1545
+ * Argv with no `--help`/`-h` token at all (the common case — an actual
1546
+ * command invocation) is returned as a shallow copy, untouched.
1547
+ */
1548
+ function rewriteHelpArgv(argv) {
1549
+ if (!argv.some((token) => token === `--help` || token === `-h`)) return [...argv];
1550
+ const path = [];
1551
+ for (const token of argv) {
1552
+ if (token.startsWith(`-`)) break;
1553
+ path.push(token);
1554
+ }
1555
+ return [`help`, ...path];
1556
+ }
1557
+ //#endregion
1172
1558
  //#region src/index.ts
1559
+ const HELP_ROUTES = /* @__PURE__ */ new Map([
1560
+ [`build`, {
1561
+ kind: `leaf`,
1562
+ commandClass: BuildCommand
1563
+ }],
1564
+ [`deploy`, {
1565
+ kind: `leaf`,
1566
+ commandClass: DeployCommand
1567
+ }],
1568
+ [`init`, {
1569
+ kind: `leaf`,
1570
+ commandClass: InitCommand
1571
+ }],
1572
+ [`migrate`, {
1573
+ kind: `leaf`,
1574
+ commandClass: MigrationCommand
1575
+ }],
1576
+ [`token`, {
1577
+ kind: `group`,
1578
+ commandClass: TokenCommand
1579
+ }],
1580
+ [`token add`, {
1581
+ kind: `leaf`,
1582
+ commandClass: TokenAddCommand
1583
+ }],
1584
+ [`token remove`, {
1585
+ kind: `leaf`,
1586
+ commandClass: TokenRemoveCommand
1587
+ }]
1588
+ ]);
1589
+ var MainHelpCommand = class extends Command {
1590
+ static paths = [
1591
+ [`-h`],
1592
+ [`--help`],
1593
+ [`help`]
1594
+ ];
1595
+ rest = Option.Rest();
1596
+ async execute() {
1597
+ if (this.rest.length === 0) {
1598
+ this.context.stdout.write(this.cli.usage());
1599
+ this.context.stdout.write(`${colors.dim(helpHint())}\n`);
1600
+ return;
1601
+ }
1602
+ const route = HELP_ROUTES.get(this.rest.join(` `));
1603
+ if (route === void 0) {
1604
+ this.writeUnknownPath(this.rest);
1605
+ return 1;
1606
+ }
1607
+ if (route.kind === `group`) {
1608
+ const command = new route.commandClass();
1609
+ command.context = this.context;
1610
+ command.cli = this.cli;
1611
+ await command.execute();
1612
+ return;
1613
+ }
1614
+ this.context.stdout.write(this.cli.usage(route.commandClass, { detailed: true }));
1615
+ this.context.stdout.write(`${colors.dim(helpHint())}\n`);
1616
+ }
1617
+ writeUnknownPath(path) {
1618
+ const format = this.cli.format();
1619
+ const label = path.join(` `);
1620
+ this.context.stdout.write(`${format.error(`Error`)}: ${t(`help.unknownPath`, { path: label })} ${t(`help.unknownPath.guide`)}\n\n`);
1621
+ this.context.stdout.write(this.cli.usage());
1622
+ this.context.stdout.write(`${colors.dim(helpHint())}\n`);
1623
+ }
1624
+ };
1173
1625
  const cli = new Cli({
1174
- binaryLabel: "appsintoss",
1175
- binaryName: "appsintoss",
1176
- enableCapture: true
1626
+ binaryLabel: "Apps in Toss CLI",
1627
+ binaryName: "ait",
1628
+ binaryVersion: "3.0.4",
1629
+ enableCapture: true,
1630
+ enableColors: isColorSupported
1177
1631
  });
1178
1632
  cli.register(BuildCommand);
1179
1633
  cli.register(DeployCommand);
@@ -1182,7 +1636,10 @@ cli.register(MigrationCommand);
1182
1636
  cli.register(TokenCommand);
1183
1637
  cli.register(TokenAddCommand);
1184
1638
  cli.register(TokenRemoveCommand);
1185
- cli.register(Builtins.HelpCommand);
1186
- cli.runExit(process.argv.slice(2));
1639
+ cli.register(MainHelpCommand);
1640
+ cli.register(Builtins.VersionCommand);
1641
+ const rawArgv = process.argv.slice(2);
1642
+ const argv = rawArgv.length === 0 ? ["--help"] : rewriteHelpArgv(rawArgv);
1643
+ cli.runExit(argv);
1187
1644
  //#endregion
1188
1645
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@apps-in-toss/cli",
3
- "version": "3.0.3",
4
- "license": "BSD-3-Clause",
3
+ "version": "3.0.4",
4
+ "license": "Apache-2.0",
5
5
  "bin": {
6
6
  "ait": "dist/index.js"
7
7
  },
@@ -23,6 +23,7 @@
23
23
  "c12": "3.3.4",
24
24
  "clipanion": "^4.0.0-rc.4",
25
25
  "jscodeshift": "^17.1.2",
26
+ "picocolors": "^1.1.1",
26
27
  "uuidv7": "^1.2.1"
27
28
  },
28
29
  "devDependencies": {