@series-inc/stowkit-cli 0.6.34 → 0.6.36

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/gcs.js CHANGED
@@ -1,69 +1,4 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import * as crypto from 'node:crypto';
4
- // ─── JWT Auth ────────────────────────────────────────────────────────────────
5
- function base64url(data) {
6
- const buf = typeof data === 'string' ? Buffer.from(data) : data;
7
- return buf.toString('base64url');
8
- }
9
- function createJWT(sa) {
10
- const now = Math.floor(Date.now() / 1000);
11
- const header = { alg: 'RS256', typ: 'JWT' };
12
- const payload = {
13
- iss: sa.client_email,
14
- scope: 'https://www.googleapis.com/auth/devstorage.read_write',
15
- aud: 'https://oauth2.googleapis.com/token',
16
- iat: now,
17
- exp: now + 3600,
18
- };
19
- const segments = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;
20
- const sign = crypto.createSign('RSA-SHA256');
21
- sign.update(segments);
22
- const signature = sign.sign(sa.private_key);
23
- return `${segments}.${base64url(signature)}`;
24
- }
25
- async function getAccessToken(sa) {
26
- const jwt = createJWT(sa);
27
- const res = await fetch('https://oauth2.googleapis.com/token', {
28
- method: 'POST',
29
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
30
- body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${jwt}`,
31
- });
32
- if (!res.ok) {
33
- const text = await res.text();
34
- throw new Error(`GCS auth failed (${res.status}): ${text}`);
35
- }
36
- const data = await res.json();
37
- return data.access_token;
38
- }
39
- // ─── Credential Resolution ───────────────────────────────────────────────────
40
- async function loadServiceAccount(projectDir) {
41
- // Search order: project dir, cwd, GOOGLE_APPLICATION_CREDENTIALS env
42
- const candidates = [
43
- path.join(projectDir, 'service_account.json'),
44
- path.join(process.cwd(), 'service_account.json'),
45
- ];
46
- for (const candidate of candidates) {
47
- try {
48
- const text = await fs.readFile(candidate, 'utf-8');
49
- return JSON.parse(text);
50
- }
51
- catch { /* not found */ }
52
- }
53
- // Fall back to GOOGLE_APPLICATION_CREDENTIALS
54
- const envPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
55
- if (envPath) {
56
- try {
57
- const text = await fs.readFile(envPath, 'utf-8');
58
- return JSON.parse(text);
59
- }
60
- catch {
61
- throw new Error(`Could not read service account from GOOGLE_APPLICATION_CREDENTIALS: ${envPath}`);
62
- }
63
- }
64
- throw new Error('No GCS credentials found. Place service_account.json in project root, ' +
65
- 'current directory, or set GOOGLE_APPLICATION_CREDENTIALS environment variable.');
66
- }
1
+ import { loadServiceAccount, getAccessToken, GCS_SCOPE } from './gcp-auth.js';
67
2
  // ─── Bucket Name Parsing ─────────────────────────────────────────────────────
68
3
  function parseBucket(bucketUri) {
69
4
  // Accept "gs://bucket-name" or just "bucket-name"
@@ -74,7 +9,7 @@ function parseBucket(bucketUri) {
74
9
  // ─── GCS Client Factory ─────────────────────────────────────────────────────
75
10
  export async function createGCSClient(projectDir, bucketUri) {
76
11
  const sa = await loadServiceAccount(projectDir);
77
- const token = await getAccessToken(sa);
12
+ const token = await getAccessToken(sa, [GCS_SCOPE]);
78
13
  const bucket = parseBucket(bucketUri);
79
14
  const apiBase = `https://storage.googleapis.com`;
80
15
  return {
@@ -109,50 +44,5 @@ export async function createGCSClient(projectDir, bucketUri) {
109
44
  }
110
45
  return res.text();
111
46
  },
112
- async downloadWithGeneration(objectPath) {
113
- const encoded = encodeURIComponent(objectPath);
114
- const url = `${apiBase}/storage/v1/b/${bucket}/o/${encoded}?alt=media`;
115
- const res = await fetch(url, {
116
- headers: { Authorization: `Bearer ${token}` },
117
- });
118
- if (res.status === 404)
119
- return null;
120
- if (!res.ok) {
121
- const text = await res.text();
122
- throw new Error(`GCS download failed for ${objectPath} (${res.status}): ${text}`);
123
- }
124
- const data = await res.text();
125
- const generation = res.headers.get('x-goog-generation') ?? '0';
126
- return { data, generation };
127
- },
128
- async uploadWithGeneration(objectPath, data, generation, contentType = 'application/json') {
129
- const encoded = encodeURIComponent(objectPath);
130
- let url = `${apiBase}/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${encoded}`;
131
- const headers = {
132
- Authorization: `Bearer ${token}`,
133
- 'Content-Type': contentType,
134
- };
135
- // Optimistic concurrency: if we have a generation, require it to match
136
- if (generation) {
137
- headers['x-goog-if-generation-match'] = generation;
138
- }
139
- else {
140
- // Object should not exist yet
141
- headers['x-goog-if-generation-match'] = '0';
142
- }
143
- const res = await fetch(url, {
144
- method: 'POST',
145
- headers,
146
- body: data,
147
- });
148
- if (res.status === 412) {
149
- throw new Error(`Registry was modified by another publish while uploading. ` +
150
- `Please retry the publish command.`);
151
- }
152
- if (!res.ok) {
153
- const text = await res.text();
154
- throw new Error(`GCS upload failed for ${objectPath} (${res.status}): ${text}`);
155
- }
156
- },
157
47
  };
158
48
  }
package/dist/index.d.ts CHANGED
@@ -30,5 +30,7 @@ export { syncRuntimeAssets } from './sync-runtime-assets.js';
30
30
  export { publishPackage } from './publish.js';
31
31
  export type { PublishOptions, PublishResult } from './publish.js';
32
32
  export * from './assets-package.js';
33
- export { fetchRegistry, searchAssets, listPackages, resolveAssetDeps } from './store.js';
33
+ export { searchAssets, listStorePackages, resolveAssetDeps } from './store.js';
34
34
  export type { SearchResult, PackageInfo } from './store.js';
35
+ export { createFirestoreReader, createFirestoreClient } from './firestore.js';
36
+ export type { FirestoreReader, FirestoreClient, FirestorePackageDoc, FirestoreVersionDoc } from './firestore.js';
package/dist/index.js CHANGED
@@ -39,4 +39,6 @@ export { syncRuntimeAssets } from './sync-runtime-assets.js';
39
39
  export { publishPackage } from './publish.js';
40
40
  export * from './assets-package.js';
41
41
  // Store
42
- export { fetchRegistry, searchAssets, listPackages, resolveAssetDeps } from './store.js';
42
+ export { searchAssets, listStorePackages, resolveAssetDeps } from './store.js';
43
+ // Firestore
44
+ export { createFirestoreReader, createFirestoreClient } from './firestore.js';
package/dist/init.js CHANGED
@@ -95,6 +95,7 @@ export async function initProject(projectDir, opts) {
95
95
  const stowkitIgnores = [
96
96
  '# StowKit',
97
97
  '*.stowcache',
98
+ '.stow-thumbnails/',
98
99
  'public/cdn-assets/',
99
100
  'Open Packer.bat',
100
101
  'open-packer.sh',
@@ -104,6 +105,16 @@ export async function initProject(projectDir, opts) {
104
105
  if (!existing.includes('*.stowcache')) {
105
106
  await fs.writeFile(gitignorePath, existing.trimEnd() + '\n\n' + stowkitIgnores + '\n');
106
107
  }
108
+ else {
109
+ // Existing project — ensure newer ignore entries are present
110
+ let updated = existing;
111
+ if (!existing.includes('.stow-thumbnails/')) {
112
+ updated = updated.trimEnd() + '\n.stow-thumbnails/\n';
113
+ }
114
+ if (updated !== existing) {
115
+ await fs.writeFile(gitignorePath, updated);
116
+ }
117
+ }
107
118
  }
108
119
  catch {
109
120
  await fs.writeFile(gitignorePath, stowkitIgnores + '\n');
@@ -118,35 +129,135 @@ export async function initProject(projectDir, opts) {
118
129
  console.log(` Config: .felicityproject`);
119
130
  console.log(` AI skills: .claude/skills/stowkit/SKILL.md, .cursor/rules/stowkit.mdc`);
120
131
  console.log(` Launcher: Open Packer.bat (Windows) / open-packer.sh (macOS/Linux)`);
132
+ // Add engine-specific gitignore entries
133
+ if (withEngine) {
134
+ const engineIgnores = [
135
+ '# Engine (generated by postinstall)',
136
+ 'node_modules/',
137
+ 'dist/',
138
+ 'public/basis/',
139
+ 'public/stowkit/',
140
+ 'public/stowkit_reader.wasm',
141
+ ].join('\n');
142
+ try {
143
+ const existing = await fs.readFile(gitignorePath, 'utf-8');
144
+ if (!existing.includes('public/stowkit_reader.wasm')) {
145
+ await fs.writeFile(gitignorePath, existing.trimEnd() + '\n\n' + engineIgnores + '\n');
146
+ }
147
+ }
148
+ catch {
149
+ // gitignore should exist from earlier step, but handle gracefully
150
+ }
151
+ }
121
152
  // Install engine if selected
122
153
  if (withEngine) {
123
154
  await installEngine(absDir);
124
155
  }
125
156
  console.log('');
126
- console.log('Drop your assets (PNG, JPG, FBX, WAV, etc.) into assets/');
127
- console.log('Then run: stowkit build');
128
- console.log('Or double-click "Open Packer.bat" to launch the packer GUI.');
157
+ if (withEngine) {
158
+ console.log('Ready to go! Run:');
159
+ console.log(' npm run dev');
160
+ }
161
+ else {
162
+ console.log('Drop your assets (PNG, JPG, FBX, WAV, etc.) into assets/');
163
+ console.log('Then run: stowkit build');
164
+ console.log('Or double-click "Open Packer.bat" to launch the packer GUI.');
165
+ }
166
+ }
167
+ async function copyTemplateFiles(templateDir, targetDir) {
168
+ const copied = [];
169
+ const entries = await fs.readdir(templateDir, { withFileTypes: true });
170
+ for (const entry of entries) {
171
+ const src = path.join(templateDir, entry.name);
172
+ const dest = path.join(targetDir, entry.name);
173
+ if (entry.isDirectory()) {
174
+ const sub = await copyTemplateFiles(src, dest);
175
+ copied.push(...sub);
176
+ }
177
+ else {
178
+ // Skip package.json — handled separately via merge logic
179
+ if (entry.name === 'package.json')
180
+ continue;
181
+ try {
182
+ await fs.access(dest);
183
+ // File exists — don't clobber
184
+ }
185
+ catch {
186
+ await fs.mkdir(path.dirname(dest), { recursive: true });
187
+ await fs.copyFile(src, dest);
188
+ copied.push(path.relative(targetDir, dest));
189
+ }
190
+ }
191
+ }
192
+ return copied;
193
+ }
194
+ async function mergePackageJson(templatePkgPath, targetDir) {
195
+ const templatePkg = JSON.parse(await fs.readFile(templatePkgPath, 'utf-8'));
196
+ const targetPkgPath = path.join(targetDir, 'package.json');
197
+ let targetPkg;
198
+ try {
199
+ targetPkg = JSON.parse(await fs.readFile(targetPkgPath, 'utf-8'));
200
+ }
201
+ catch {
202
+ // No existing package.json — use template as-is
203
+ templatePkg.name = path.basename(targetDir);
204
+ await fs.writeFile(targetPkgPath, JSON.stringify(templatePkg, null, 2) + '\n');
205
+ return;
206
+ }
207
+ // Merge scripts (don't overwrite existing scripts)
208
+ targetPkg.scripts = { ...templatePkg.scripts, ...targetPkg.scripts };
209
+ // Ensure postinstall includes copy-decoders
210
+ if (!targetPkg.scripts.postinstall?.includes('copy-decoders')) {
211
+ targetPkg.scripts.postinstall = templatePkg.scripts.postinstall;
212
+ }
213
+ // Merge dependencies
214
+ for (const depType of ['dependencies', 'devDependencies', 'peerDependencies']) {
215
+ if (templatePkg[depType]) {
216
+ targetPkg[depType] = { ...templatePkg[depType], ...(targetPkg[depType] ?? {}) };
217
+ }
218
+ }
219
+ // Ensure type: module
220
+ if (!targetPkg.type)
221
+ targetPkg.type = 'module';
222
+ await fs.writeFile(targetPkgPath, JSON.stringify(targetPkg, null, 2) + '\n');
129
223
  }
130
224
  async function installEngine(absDir) {
225
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
226
+ const templateDir = path.resolve(thisDir, '../templates/engine');
227
+ // Verify template directory exists
228
+ try {
229
+ await fs.access(templateDir);
230
+ }
231
+ catch {
232
+ console.error('Engine template not found. Please update @series-inc/stowkit-cli.');
233
+ return;
234
+ }
131
235
  console.log('');
132
- console.log('Installing @series-inc/rundot-3d-engine and three...');
236
+ console.log('Setting up 3D engine project...');
237
+ // Copy template files (skip existing, skip package.json)
238
+ const copied = await copyTemplateFiles(templateDir, absDir);
239
+ if (copied.length > 0) {
240
+ console.log(` Created: ${copied.join(', ')}`);
241
+ }
242
+ // Merge or create package.json
243
+ await mergePackageJson(path.join(templateDir, 'package.json'), absDir);
244
+ // Install dependencies
245
+ console.log(' Installing dependencies...');
133
246
  const { execSync } = await import('node:child_process');
134
247
  try {
135
- execSync('npm install @series-inc/rundot-3d-engine three', {
248
+ execSync('npm install', {
136
249
  cwd: absDir,
137
250
  stdio: 'inherit',
138
251
  });
139
252
  }
140
253
  catch {
141
- console.error('Failed to install engine packages. You can install manually:');
142
- console.error(' npm install @series-inc/rundot-3d-engine three');
254
+ console.error('Failed to install dependencies. Run `npm install` manually.');
143
255
  return;
144
256
  }
257
+ // Copy engine skill files
145
258
  await copyEngineSkillFiles(absDir);
146
259
  console.log('');
147
- console.log(' 3D Engine installed:');
148
- console.log(' @series-inc/rundot-3d-engine');
149
- console.log(' three');
260
+ console.log(' 3D Engine project ready!');
150
261
  console.log(' AI skills: .claude/skills/stowkit-engine/SKILL.md, .cursor/rules/stowkit-engine.mdc');
151
262
  }
152
263
  async function copySkillFiles(absDir) {
package/dist/publish.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { readProjectConfig, scanDirectory } from './node-fs.js';
4
- import { readAssetsPackage, initAssetsPackage, createEmptyRegistry, } from './assets-package.js';
4
+ import { readAssetsPackage, initAssetsPackage, } from './assets-package.js';
5
+ import { createFirestoreClient } from './firestore.js';
5
6
  import { createGCSClient } from './gcs.js';
6
7
  import { readStowmeta } from './app/stowmeta-io.js';
7
8
  // ─── Build full dependency graph from srcArtDir ──────────────────────────────
@@ -145,7 +146,7 @@ export async function publishPackage(projectDir, opts = {}) {
145
146
  const bucketUri = opts.bucket
146
147
  ?? pkg.bucket
147
148
  ?? process.env.STOWKIT_BUCKET
148
- ?? 'gs://venus-shared-assets-test';
149
+ ?? 'gs://rungame-shared-assets-test';
149
150
  log(`Bucket: ${bucketUri}`);
150
151
  // Step 4: Scan srcArtDir
151
152
  const scan = await scanDirectory(project.srcArtDir);
@@ -252,29 +253,15 @@ export async function publishPackage(projectDir, opts = {}) {
252
253
  }
253
254
  // Step 7: Create GCS client and auth
254
255
  const emitProgress = opts.onProgress ?? (() => { });
255
- const totalUploads = filesToUpload.length + Object.keys(thumbMap).length + 2; // +2 for assets-package.json and registry.json
256
+ const totalUploads = filesToUpload.length + Object.keys(thumbMap).length + 2; // +2 for assets-package.json and Firestore writes
256
257
  let totalDone = 0;
257
258
  log('Authenticating with GCS...');
258
259
  emitProgress({ phase: 'auth', done: 0, total: totalUploads, message: 'Authenticating with GCS...' });
259
260
  const gcs = await createGCSClient(projectDir, bucketUri);
260
- // Step 8: Download current registry
261
- log('Fetching registry...');
262
- const registryResult = await gcs.downloadWithGeneration('registry.json');
263
- let registry;
264
- let generation;
265
- if (registryResult) {
266
- registry = JSON.parse(registryResult.data);
267
- generation = registryResult.generation;
268
- vlog(`Registry loaded (generation: ${generation})`);
269
- }
270
- else {
271
- registry = createEmptyRegistry();
272
- generation = null;
273
- vlog('No existing registry — will create new one');
274
- }
275
- // Step 9: Check version not already published
276
- const existingPkg = registry.packages[pkg.name];
277
- if (existingPkg?.versions[pkg.version] && !opts.force) {
261
+ // Step 8: Create Firestore client and check version
262
+ const firestore = await createFirestoreClient(projectDir);
263
+ const existingVer = await firestore.getVersion(pkg.name, pkg.version);
264
+ if (existingVer && !opts.force) {
278
265
  throw new Error(`Version ${pkg.version} of "${pkg.name}" is already published. ` +
279
266
  `Bump the version in assets-package.json or use --force to overwrite.`);
280
267
  }
@@ -338,27 +325,17 @@ export async function publishPackage(projectDir, opts = {}) {
338
325
  }
339
326
  catch { /* file doesn't exist, try next */ }
340
327
  }
341
- // Step 13: Update and upload registry
328
+ // Step 13: Write to Firestore
342
329
  log('Updating registry...');
343
- if (!registry.packages[pkg.name]) {
344
- registry.packages[pkg.name] = {
345
- description: pkg.description,
346
- author: pkg.author,
347
- tags: pkg.tags ?? [],
348
- latest: pkg.version,
349
- versions: {},
350
- };
351
- }
352
- const regPkg = registry.packages[pkg.name];
353
- regPkg.versions[pkg.version] = versionEntry;
354
- regPkg.latest = pkg.version;
355
- regPkg.description = pkg.description;
356
- regPkg.author = pkg.author;
357
- regPkg.tags = pkg.tags ?? [];
358
- if (packThumbnail)
359
- regPkg.thumbnail = packThumbnail;
360
330
  emitProgress({ phase: 'registry', done: totalDone, total: totalUploads, message: 'Updating registry...' });
361
- await gcs.uploadWithGeneration('registry.json', JSON.stringify(registry, null, 2), generation);
331
+ await firestore.setVersion(pkg.name, pkg.version, versionEntry);
332
+ await firestore.setPackage(pkg.name, {
333
+ description: pkg.description,
334
+ author: pkg.author,
335
+ tags: pkg.tags ?? [],
336
+ latest: pkg.version,
337
+ thumbnail: packThumbnail ?? null,
338
+ });
362
339
  totalDone++;
363
340
  emitProgress({ phase: 'registry', done: totalDone, total: totalUploads, message: 'Done' });
364
341
  log(`\nPublished ${pkg.name}@${pkg.version} successfully!`);