@uzuhq/code-cli 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/auth/browser.js +22 -0
- package/dist/auth/config.js +48 -0
- package/dist/auth/env.js +42 -0
- package/dist/auth/jwt.js +27 -0
- package/dist/auth/login-flow.js +44 -0
- package/dist/auth/loopback.js +94 -0
- package/dist/auth/pkce.js +11 -0
- package/dist/auth/publish-token.js +74 -0
- package/dist/auth/token-cache.js +70 -0
- package/dist/auth/uzu-auth.js +94 -0
- package/dist/build-server-logic.js +28 -0
- package/dist/cf-images-upload.js +52 -0
- package/dist/cli.js +303 -0
- package/dist/create-2d-game.js +56 -0
- package/dist/dev-server/game-room.js +436 -0
- package/dist/dev-server/game-types.js +11 -0
- package/dist/dev-server/json-patch.js +114 -0
- package/dist/dev-server/load-logic.js +70 -0
- package/dist/dev-server/random.js +35 -0
- package/dist/dev-server/relay-room.js +84 -0
- package/dist/dev-server/server.js +367 -0
- package/dist/dev-server/sync-room.js +268 -0
- package/dist/dev.js +235 -0
- package/dist/harness/admin-client.js +215 -0
- package/dist/harness/client-entry.js +90 -0
- package/dist/harness/dev-button.js +249 -0
- package/dist/harness/mount.js +664 -0
- package/dist/harness/page.js +46 -0
- package/dist/r2-upload.js +93 -0
- package/dist/rest-register.js +50 -0
- package/dist/upload-session.js +71 -0
- package/game-2d-template/index.html.tpl +18 -0
- package/game-2d-template/manifest.json.tpl +6 -0
- package/game-2d-template/package.json.tpl +23 -0
- package/game-2d-template/src/main.ts +49 -0
- package/game-2d-template/src/vite-env.d.ts +1 -0
- package/game-2d-template/tsconfig.json +12 -0
- package/game-2d-template/vite.config.ts +6 -0
- package/package.json +43 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { createUploadSession } from './upload-session.js';
|
|
4
|
+
/**
|
|
5
|
+
* ローカルのアイコン群を Cloudflare Images にアップロードし、
|
|
6
|
+
* ファイルパス → 配信 URL の対応を返す。
|
|
7
|
+
*
|
|
8
|
+
* one-time upload URL は backend がアップロードセッション経由で発行するため、
|
|
9
|
+
* CLI は Cloudflare Images のクレデンシャルを持たない。
|
|
10
|
+
* 最終的な画像 ID は one-time URL の draft ID とは別で、アップロードレスポンスの
|
|
11
|
+
* `result.id` でしか分からないため、配信 URL はアップロード後に組み立てる。
|
|
12
|
+
* 返す URL は Icon resolver が "/original" を付加する前提の
|
|
13
|
+
* `https://imagedelivery.net/.../imageId` 形式。
|
|
14
|
+
*/
|
|
15
|
+
/** アップロードレスポンスから最終的な画像 ID を取り出す。 */
|
|
16
|
+
export const parseUploadedImageId = (v) => {
|
|
17
|
+
if (typeof v === 'object' &&
|
|
18
|
+
v !== null &&
|
|
19
|
+
'success' in v &&
|
|
20
|
+
v.success === true &&
|
|
21
|
+
'result' in v &&
|
|
22
|
+
typeof v.result === 'object' &&
|
|
23
|
+
v.result !== null &&
|
|
24
|
+
'id' in v.result &&
|
|
25
|
+
typeof v.result.id === 'string' &&
|
|
26
|
+
v.result.id !== '') {
|
|
27
|
+
return v.result.id;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Cloudflare Images の応答が想定外の形式です: ${JSON.stringify(v)}`);
|
|
30
|
+
};
|
|
31
|
+
export const uploadIconsToCfImages = async (env, token, filePaths) => {
|
|
32
|
+
if (filePaths.length === 0)
|
|
33
|
+
return new Map();
|
|
34
|
+
const session = await createUploadSession({ env, token, imageCount: filePaths.length });
|
|
35
|
+
if (session.images.length !== filePaths.length) {
|
|
36
|
+
throw new Error('アイコンのアップロード URL の発行数が一致しません');
|
|
37
|
+
}
|
|
38
|
+
const result = new Map();
|
|
39
|
+
await Promise.all(filePaths.map(async (filePath, i) => {
|
|
40
|
+
const image = session.images[i];
|
|
41
|
+
const formData = new FormData();
|
|
42
|
+
formData.append('file', new Blob([readFileSync(filePath)]), basename(filePath));
|
|
43
|
+
const res = await fetch(image.uploadURL, { method: 'POST', body: formData });
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const detail = (await res.text()).trim();
|
|
46
|
+
throw new Error(`Cloudflare Images アップロード失敗 (${filePath}, HTTP ${res.status}): ${detail}`);
|
|
47
|
+
}
|
|
48
|
+
const imageId = parseUploadedImageId(await res.json());
|
|
49
|
+
result.set(filePath, `${session.imageDeliveryBaseURL}/${imageId}`);
|
|
50
|
+
}));
|
|
51
|
+
return result;
|
|
52
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @docs
|
|
4
|
+
* - ゲーム仕様: docs/docs/play_screen_v3/games.md
|
|
5
|
+
* - システム全体像: docs/docs/play_screen_v3/overview.md
|
|
6
|
+
*/
|
|
7
|
+
import { Command, Option } from 'commander';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { readFileSync, existsSync, unlinkSync, createWriteStream } from 'fs';
|
|
10
|
+
import { resolve } from 'path';
|
|
11
|
+
import { ZipArchive } from 'archiver';
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
13
|
+
import { uploadGameToR2, uploadLogicToR2 } from './r2-upload.js';
|
|
14
|
+
import { registerRevision } from './rest-register.js';
|
|
15
|
+
import { buildServerLogic } from './build-server-logic.js';
|
|
16
|
+
import { create2dGame } from './create-2d-game.js';
|
|
17
|
+
import { uploadIconsToCfImages } from './cf-images-upload.js';
|
|
18
|
+
import { runDevCommand } from './dev.js';
|
|
19
|
+
import { DEFAULT_ENV, authBaseURL, resolveEnv, studioHost } from './auth/env.js';
|
|
20
|
+
import { runLoginFlow } from './auth/login-flow.js';
|
|
21
|
+
import { saveCredentials, clearCredentials, credentialsPath } from './auth/config.js';
|
|
22
|
+
import { getLoginIdToken, getValidIdToken } from './auth/token-cache.js';
|
|
23
|
+
import { PUBLISH_TOKEN_ENV, createPublishToken, listPublishTokens, revokePublishToken, } from './auth/publish-token.js';
|
|
24
|
+
// publish / login / logout 共通の env オプション。既定 dev・help には出さない (誤って本番へ publish しないため)。
|
|
25
|
+
const envOption = () => new Option('--env <env>', '接続先環境 (dev | stg | prd)').default(DEFAULT_ENV).hideHelp();
|
|
26
|
+
const program = new Command();
|
|
27
|
+
program.name('uzu').description('UZU ゲーム開発 CLI').version('0.2.0');
|
|
28
|
+
program
|
|
29
|
+
.command('create-2d-game')
|
|
30
|
+
.description('2D エンジンを使ったゲームプロジェクトの雛形を作成')
|
|
31
|
+
.argument('<name>', 'プロジェクト名(ディレクトリ名)')
|
|
32
|
+
.action((name) => {
|
|
33
|
+
create2dGame(name);
|
|
34
|
+
});
|
|
35
|
+
program
|
|
36
|
+
.command('dev')
|
|
37
|
+
.description('scenario の dev server を起動し、 dev harness (iframe grid + HUD) と ' +
|
|
38
|
+
'in-memory GameRoom / SyncRoom / RelayRoom を提供する')
|
|
39
|
+
.action(async () => {
|
|
40
|
+
try {
|
|
41
|
+
await runDevCommand();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error('[uzu dev]', err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
program
|
|
49
|
+
.command('publish')
|
|
50
|
+
.description('ゲームをビルドして R2 にアップロード → 登録')
|
|
51
|
+
.option('--change-notes <msg>', 'リビジョンの変更メモ', '')
|
|
52
|
+
.addOption(envOption())
|
|
53
|
+
.action(async (options) => {
|
|
54
|
+
const env = resolveEnv(options.env);
|
|
55
|
+
const cwd = process.cwd();
|
|
56
|
+
const manifestPath = resolve(cwd, 'manifest.json');
|
|
57
|
+
if (!existsSync(manifestPath)) {
|
|
58
|
+
console.error('manifest.json が見つかりません。ゲームディレクトリで実行してください。');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
62
|
+
if (typeof manifest !== 'object' || manifest === null) {
|
|
63
|
+
console.error('manifest.json が不正な形式です。');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
if (!manifest.id || !manifest.playerCount || !manifest.output) {
|
|
67
|
+
console.error('manifest.json に id と playerCount と output が必要です。');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const gameId = manifest.id;
|
|
71
|
+
const manifestCharacters = manifest.characters;
|
|
72
|
+
// characters バリデーション
|
|
73
|
+
if (manifestCharacters !== undefined) {
|
|
74
|
+
if (manifestCharacters.length === 0) {
|
|
75
|
+
console.error('characters が空の配列です。');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
for (const c of manifestCharacters) {
|
|
79
|
+
if (!c.id || !c.name) {
|
|
80
|
+
console.error('characters の各要素に id と name が必要です。');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// playerCount: characters があればその長さ、なければ manifest.playerCount
|
|
86
|
+
const playerCount = manifestCharacters?.length ?? manifest.playerCount;
|
|
87
|
+
// ビルド・アップロード後に backend の 400 で落ちると手戻りが大きいので、ここで検証する
|
|
88
|
+
const rawOrientation = manifest.orientation ?? 'portrait';
|
|
89
|
+
if (rawOrientation !== 'portrait' && rawOrientation !== 'landscape') {
|
|
90
|
+
console.error(`manifest.json の orientation は portrait か landscape を指定してください (指定値: ${String(rawOrientation)})`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const orientation = rawOrientation;
|
|
94
|
+
const buildCommand = manifest.build;
|
|
95
|
+
const outputDir = manifest.output;
|
|
96
|
+
console.log(`Publishing game: ${gameId} (players: ${playerCount}, orientation: ${orientation})`);
|
|
97
|
+
// 認証はビルド前に確認する。未ログインのまま重いビルド・アップロードまで進むと、
|
|
98
|
+
// 完了後に登録で失敗して成果物が無駄になるため、ここで先に落とす。
|
|
99
|
+
await getValidIdToken(env).catch((e) => {
|
|
100
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
103
|
+
// 1. Build
|
|
104
|
+
if (buildCommand) {
|
|
105
|
+
console.log('Building...');
|
|
106
|
+
execSync(buildCommand, { stdio: 'inherit', cwd });
|
|
107
|
+
}
|
|
108
|
+
// 2. ZIP (archiver で OS 非依存に生成)
|
|
109
|
+
console.log('Creating ZIP...');
|
|
110
|
+
const absoluteOutputDir = resolve(cwd, outputDir);
|
|
111
|
+
const zipPath = resolve(cwd, '__zip__.zip');
|
|
112
|
+
await new Promise((res, reject) => {
|
|
113
|
+
const output = createWriteStream(zipPath);
|
|
114
|
+
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
115
|
+
output.on('close', () => res());
|
|
116
|
+
archive.on('error', (err) => reject(err));
|
|
117
|
+
archive.pipe(output);
|
|
118
|
+
archive.directory(absoluteOutputDir, false);
|
|
119
|
+
archive.finalize();
|
|
120
|
+
});
|
|
121
|
+
console.log(`Created: ${zipPath}`);
|
|
122
|
+
// 3. R2 Upload (ZIP + 個別ファイル)
|
|
123
|
+
// ZIP はモバイル `/__zip__` 経路用、個別ファイルは Web emulator 経路用。
|
|
124
|
+
// Worker のメモリ上限 (128MB) を超えないよう、Worker 側で ZIP を展開せずに
|
|
125
|
+
// R2 から直接個別ファイルを返す前提で publish 時に展開済みオブジェクトを並べる。
|
|
126
|
+
console.log('Uploading to R2...');
|
|
127
|
+
const { resourceId, revisionId, token } = await uploadGameToR2(env, zipPath, absoluteOutputDir);
|
|
128
|
+
console.log(`Uploaded to R2. revisionId: ${revisionId}, resourceId: ${resourceId}`);
|
|
129
|
+
// 4. ServerAction — serverActionLogicPath があれば logic.js ビルド + R2 保存
|
|
130
|
+
if (manifest.serverActionLogicPath) {
|
|
131
|
+
const logicEntryPoint = resolve(cwd, manifest.serverActionLogicPath);
|
|
132
|
+
const logicOutPath = resolve(cwd, '__logic__.js');
|
|
133
|
+
try {
|
|
134
|
+
console.log('Building server logic...');
|
|
135
|
+
await buildServerLogic(logicEntryPoint, logicOutPath);
|
|
136
|
+
// ビルド済み logic.js を動的 import し、default or named "logic" が存在するか検証
|
|
137
|
+
// Windows では絶対パスをそのまま渡すと ERR_UNSUPPORTED_ESM_URL_SCHEME になるため file:// URL に変換する
|
|
138
|
+
const logicModule = await import(pathToFileURL(logicOutPath).href);
|
|
139
|
+
const resolvedLogic = logicModule.default ?? logicModule.logic;
|
|
140
|
+
if (!resolvedLogic) {
|
|
141
|
+
console.error(`Error: ${manifest.serverActionLogicPath} must export a GameLogic object.\n` +
|
|
142
|
+
` Use either: export default logic\n` +
|
|
143
|
+
` Or: export const logic: GameLogic<State> = { ... }`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
// ゲーム固有の logic.js のみを R2 に保存する。
|
|
147
|
+
// ランタイムテンプレ(GameRoom エンジン)は v3-play-server が所有し、JIT デプロイ時に
|
|
148
|
+
// この logic.js と合成される。これによりテンプレ修正が再 publish 無しで伝播する。
|
|
149
|
+
console.log('Uploading logic.js to R2...');
|
|
150
|
+
const logicJsContent = readFileSync(logicOutPath, 'utf-8');
|
|
151
|
+
await uploadLogicToR2(env, token, logicJsContent);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
if (existsSync(logicOutPath))
|
|
155
|
+
unlinkSync(logicOutPath);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 5. Resolve character icons (相対パス → Cloudflare Images アップロード)
|
|
159
|
+
let resolvedCharacters;
|
|
160
|
+
if (manifestCharacters) {
|
|
161
|
+
console.log('Resolving character icons...');
|
|
162
|
+
const isLocalIcon = (char) => !!char.icon && !char.icon.startsWith('http://') && !char.icon.startsWith('https://');
|
|
163
|
+
const localIconPaths = manifestCharacters.filter(isLocalIcon).map((char) => {
|
|
164
|
+
const iconAbsPath = resolve(cwd, char.icon);
|
|
165
|
+
if (!existsSync(iconAbsPath)) {
|
|
166
|
+
console.error(`アイコンファイルが見つかりません: ${char.icon}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
return iconAbsPath;
|
|
170
|
+
});
|
|
171
|
+
const iconURLs = await uploadIconsToCfImages(env, token, localIconPaths);
|
|
172
|
+
resolvedCharacters = manifestCharacters.map((char) => {
|
|
173
|
+
if (!isLocalIcon(char))
|
|
174
|
+
return char;
|
|
175
|
+
const cfImagesUrl = iconURLs.get(resolve(cwd, char.icon));
|
|
176
|
+
if (!cfImagesUrl) {
|
|
177
|
+
console.error(`アイコンのアップロード結果が見つかりません: ${char.icon}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
console.log(`Uploaded icon: ${char.icon}`);
|
|
181
|
+
return { ...char, icon: cfImagesUrl };
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// 6. Register revision
|
|
185
|
+
console.log('Registering revision...');
|
|
186
|
+
const revId = await registerRevision({
|
|
187
|
+
env,
|
|
188
|
+
gameId,
|
|
189
|
+
resourceId,
|
|
190
|
+
uploadToken: token,
|
|
191
|
+
changeNotes: options.changeNotes || '',
|
|
192
|
+
playerCount,
|
|
193
|
+
orientation,
|
|
194
|
+
characters: resolvedCharacters,
|
|
195
|
+
});
|
|
196
|
+
console.log(`Registered revision: ${revId}`);
|
|
197
|
+
// 7. Cleanup
|
|
198
|
+
unlinkSync(zipPath);
|
|
199
|
+
const studioUrl = `https://${studioHost(env)}/ja/scenarios/global-id/${gameId}`;
|
|
200
|
+
console.log('\nDone!');
|
|
201
|
+
console.log(`UZU Studio: ${studioUrl}`);
|
|
202
|
+
});
|
|
203
|
+
program
|
|
204
|
+
.command('login')
|
|
205
|
+
.description('UZU にログインして publish 用の認証情報を保存')
|
|
206
|
+
.addOption(envOption())
|
|
207
|
+
.action(async (options) => {
|
|
208
|
+
const env = resolveEnv(options.env);
|
|
209
|
+
let result;
|
|
210
|
+
try {
|
|
211
|
+
result = await runLoginFlow(env, (url) => {
|
|
212
|
+
console.log('ブラウザで Google ログインを開きます。');
|
|
213
|
+
console.log('自動で開かない場合は次の URL を踏んでください:');
|
|
214
|
+
console.log(` ${url}`);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
console.error(`ログインに失敗しました: ${e.message}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
await saveCredentials(env, {
|
|
222
|
+
uid: result.uid,
|
|
223
|
+
email: result.email,
|
|
224
|
+
refreshToken: result.refreshToken,
|
|
225
|
+
loggedInAt: new Date().toISOString(),
|
|
226
|
+
});
|
|
227
|
+
console.log(`\nログインしました: ${result.email} (env: ${env})`);
|
|
228
|
+
console.log(`保存先: ${credentialsPath()} (mode 0600)`);
|
|
229
|
+
});
|
|
230
|
+
program
|
|
231
|
+
.command('logout')
|
|
232
|
+
.description('保存済みの認証情報を削除')
|
|
233
|
+
.addOption(envOption())
|
|
234
|
+
.action(async (options) => {
|
|
235
|
+
const env = resolveEnv(options.env);
|
|
236
|
+
const removed = await clearCredentials(env);
|
|
237
|
+
if (removed) {
|
|
238
|
+
console.log(`ログアウトしました (env: ${env})`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.log(`ログイン情報はありません (env: ${env})`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
// CI / 自動化用の publish token 管理。認証は `uzu login` 済みの認証情報のみで、
|
|
245
|
+
// UZU_PUBLISH_TOKEN では操作できない (漏れたトークンから新しい token を生やさせない)。
|
|
246
|
+
const tokenCommand = program.command('token').description('CI 用 publish token の管理');
|
|
247
|
+
const runTokenCommand = async (rawEnv, fn) => {
|
|
248
|
+
const env = resolveEnv(rawEnv);
|
|
249
|
+
try {
|
|
250
|
+
const idToken = await getLoginIdToken(env);
|
|
251
|
+
await fn({ env, baseUrl: authBaseURL(env), idToken });
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
tokenCommand
|
|
259
|
+
.command('create')
|
|
260
|
+
.description('publish token を発行する (平文はこの 1 回しか表示されない)')
|
|
261
|
+
.option('--name <label>', 'トークンの用途がわかる表示名', '')
|
|
262
|
+
.addOption(envOption())
|
|
263
|
+
.action(async (options) => {
|
|
264
|
+
await runTokenCommand(options.env, async ({ env, baseUrl, idToken }) => {
|
|
265
|
+
const issued = await createPublishToken(baseUrl, idToken, options.name);
|
|
266
|
+
console.log(`publish token を発行しました (env: ${env})`);
|
|
267
|
+
console.log(` id: ${issued.id}`);
|
|
268
|
+
console.log(` name: ${issued.name || '(なし)'}`);
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log(` ${issued.token}`);
|
|
271
|
+
console.log('');
|
|
272
|
+
console.log('この平文は再表示できません。CI では secret に登録し、');
|
|
273
|
+
console.log(`${PUBLISH_TOKEN_ENV} として渡してください。`);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
tokenCommand
|
|
277
|
+
.command('list')
|
|
278
|
+
.description('有効な publish token を一覧する')
|
|
279
|
+
.addOption(envOption())
|
|
280
|
+
.action(async (options) => {
|
|
281
|
+
await runTokenCommand(options.env, async ({ env, baseUrl, idToken }) => {
|
|
282
|
+
const tokens = await listPublishTokens(baseUrl, idToken);
|
|
283
|
+
if (tokens.length === 0) {
|
|
284
|
+
console.log(`publish token はありません (env: ${env})`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
for (const t of tokens) {
|
|
288
|
+
console.log(`${t.id} ${t.name || '(なし)'} created=${t.createdAt} lastUsed=${t.lastUsedAt ?? '-'}`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
tokenCommand
|
|
293
|
+
.command('revoke')
|
|
294
|
+
.description('publish token を失効させる')
|
|
295
|
+
.argument('<id>', '失効させる token の id (uzu token list で確認)')
|
|
296
|
+
.addOption(envOption())
|
|
297
|
+
.action(async (id, options) => {
|
|
298
|
+
await runTokenCommand(options.env, async ({ env, baseUrl, idToken }) => {
|
|
299
|
+
await revokePublishToken(baseUrl, idToken, id);
|
|
300
|
+
console.log(`失効しました: ${id} (env: ${env})`);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
program.parse();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { resolve, dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
/** テンプレートディレクトリから再帰的にファイルをコピー。.tpl ファイルは置換処理を行う。 */
|
|
6
|
+
const copyDir = (src, dest, replacements) => {
|
|
7
|
+
mkdirSync(dest, { recursive: true });
|
|
8
|
+
for (const entry of readdirSync(src)) {
|
|
9
|
+
const srcPath = join(src, entry);
|
|
10
|
+
const stat = statSync(srcPath);
|
|
11
|
+
if (stat.isDirectory()) {
|
|
12
|
+
copyDir(srcPath, join(dest, entry), replacements);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (entry.endsWith('.tpl')) {
|
|
16
|
+
// テンプレートファイル: プレースホルダーを置換して拡張子を除いた名前で出力
|
|
17
|
+
let content = readFileSync(srcPath, 'utf-8');
|
|
18
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
19
|
+
content = content.replaceAll(`{{${key}}}`, value);
|
|
20
|
+
}
|
|
21
|
+
writeFileSync(join(dest, entry.replace(/\.tpl$/, '')), content);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// 通常ファイル: テキストなら置換、バイナリならそのままコピー
|
|
25
|
+
const content = readFileSync(srcPath, 'utf-8');
|
|
26
|
+
let output = content;
|
|
27
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
28
|
+
output = output.replaceAll(`{{${key}}}`, value);
|
|
29
|
+
}
|
|
30
|
+
writeFileSync(join(dest, entry), output);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** kebab-case をタイトルケースに変換 */
|
|
35
|
+
const toTitle = (name) => name
|
|
36
|
+
.split('-')
|
|
37
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
38
|
+
.join(' ');
|
|
39
|
+
export const create2dGame = (name) => {
|
|
40
|
+
const dest = resolve(process.cwd(), name);
|
|
41
|
+
const templateDir = resolve(__dirname, '..', 'game-2d-template');
|
|
42
|
+
const title = toTitle(name);
|
|
43
|
+
const replacements = { name, title };
|
|
44
|
+
console.log(`Creating 2D game project: ${name}`);
|
|
45
|
+
copyDir(templateDir, dest, replacements);
|
|
46
|
+
console.log(`\nDone! Created ${name}/`);
|
|
47
|
+
console.log(`\nNext steps:`);
|
|
48
|
+
console.log(` cd ${name}`);
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(' # ~/.npmrc に GitHub Packages の認証設定がない場合:');
|
|
51
|
+
console.log(' echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN" >> ~/.npmrc');
|
|
52
|
+
console.log(' echo "@uzupj:registry=https://npm.pkg.github.com" >> ~/.npmrc');
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(` npm install`);
|
|
55
|
+
console.log(` npm run dev`);
|
|
56
|
+
};
|