create-objectstack 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,720 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { execSync } from 'child_process';
8
+
9
+ // ─── Template Registry ──────────────────────────────────────────────
10
+
11
+ type TemplateFiles = Record<string, (name: string) => string>;
12
+
13
+ interface Template {
14
+ description: string;
15
+ files: TemplateFiles;
16
+ }
17
+
18
+ const TEMPLATES: Record<string, Template> = {
19
+ 'minimal-api': {
20
+ description: 'Server + memory driver + 1 object + REST API',
21
+ files: {
22
+ 'objectstack.config.ts': (name) => `import { defineStack } from '@objectstack/spec';
23
+ import * as objects from './src/objects';
24
+
25
+ export default defineStack({
26
+ manifest: {
27
+ id: 'com.example.${name}',
28
+ namespace: '${name}',
29
+ version: '0.1.0',
30
+ type: 'app',
31
+ name: '${toTitleCase(name)}',
32
+ description: '${toTitleCase(name)} — built with ObjectStack',
33
+ },
34
+
35
+ objects: Object.values(objects),
36
+
37
+ api: {
38
+ rest: { enabled: true, basePath: '/api' },
39
+ },
40
+ });
41
+ `,
42
+ 'package.json': (name) => JSON.stringify({
43
+ name,
44
+ version: '0.1.0',
45
+ private: true,
46
+ type: 'module',
47
+ scripts: {
48
+ dev: 'objectstack dev',
49
+ start: 'objectstack serve',
50
+ build: 'objectstack compile',
51
+ validate: 'objectstack validate',
52
+ typecheck: 'tsc --noEmit',
53
+ },
54
+ dependencies: {
55
+ '@objectstack/spec': '^3.0.0',
56
+ '@objectstack/runtime': '^3.0.0',
57
+ '@objectstack/driver-memory': '^3.0.0',
58
+ '@objectstack/plugin-hono-server': '^3.0.0',
59
+ },
60
+ devDependencies: {
61
+ '@objectstack/cli': '^3.0.0',
62
+ 'typescript': '^5.3.0',
63
+ },
64
+ }, null, 2) + '\n',
65
+ 'tsconfig.json': () => JSON.stringify({
66
+ compilerOptions: {
67
+ target: 'ES2022',
68
+ module: 'ESNext',
69
+ moduleResolution: 'bundler',
70
+ strict: true,
71
+ esModuleInterop: true,
72
+ skipLibCheck: true,
73
+ outDir: 'dist',
74
+ rootDir: '.',
75
+ declaration: true,
76
+ },
77
+ include: ['*.ts', 'src/**/*'],
78
+ exclude: ['dist', 'node_modules'],
79
+ }, null, 2) + '\n',
80
+ 'src/objects/task.ts': () => `import { Data } from '@objectstack/spec';
81
+
82
+ const task: Data.Object = {
83
+ name: 'task',
84
+ label: 'Task',
85
+ ownership: 'own',
86
+ fields: {
87
+ title: {
88
+ type: 'text',
89
+ label: 'Title',
90
+ required: true,
91
+ },
92
+ description: {
93
+ type: 'textarea',
94
+ label: 'Description',
95
+ },
96
+ status: {
97
+ type: 'select',
98
+ label: 'Status',
99
+ options: [
100
+ { label: 'Open', value: 'open' },
101
+ { label: 'In Progress', value: 'in_progress' },
102
+ { label: 'Done', value: 'done' },
103
+ ],
104
+ defaultValue: 'open',
105
+ },
106
+ due_date: {
107
+ type: 'date',
108
+ label: 'Due Date',
109
+ },
110
+ },
111
+ };
112
+
113
+ export default task;
114
+ `,
115
+ 'src/objects/index.ts': () => `export { default as task } from './task';
116
+ `,
117
+ '.gitignore': () => `node_modules/
118
+ dist/
119
+ *.tsbuildinfo
120
+ `,
121
+ 'README.md': (name) => `# ${toTitleCase(name)}
122
+
123
+ Built with [ObjectStack](https://objectstack.com).
124
+
125
+ ## Quick Start
126
+
127
+ \`\`\`bash
128
+ # Install dependencies
129
+ npm install
130
+
131
+ # Start development server
132
+ npm run dev
133
+
134
+ # Validate configuration
135
+ npm run validate
136
+ \`\`\`
137
+
138
+ ## Project Structure
139
+
140
+ - \`objectstack.config.ts\` — Stack definition (objects, API, settings)
141
+ - \`src/objects/\` — Object definitions
142
+ - \`dist/\` — Compiled output
143
+
144
+ ## Learn More
145
+
146
+ - [ObjectStack Documentation](https://objectstack.com/docs)
147
+ `,
148
+ },
149
+ },
150
+
151
+ 'full-stack': {
152
+ description: 'Server + UI + auth + 3 CRM objects',
153
+ files: {
154
+ 'objectstack.config.ts': (name) => `import { defineStack } from '@objectstack/spec';
155
+ import * as objects from './src/objects';
156
+ import * as apps from './src/apps';
157
+
158
+ export default defineStack({
159
+ manifest: {
160
+ id: 'com.example.${name}',
161
+ namespace: '${name}',
162
+ version: '0.1.0',
163
+ type: 'app',
164
+ name: '${toTitleCase(name)}',
165
+ description: '${toTitleCase(name)} CRM — built with ObjectStack',
166
+ },
167
+
168
+ objects: Object.values(objects),
169
+ apps: Object.values(apps),
170
+
171
+ api: {
172
+ rest: { enabled: true, basePath: '/api' },
173
+ },
174
+ });
175
+ `,
176
+ 'package.json': (name) => JSON.stringify({
177
+ name,
178
+ version: '0.1.0',
179
+ private: true,
180
+ type: 'module',
181
+ scripts: {
182
+ dev: 'objectstack dev',
183
+ start: 'objectstack serve',
184
+ build: 'objectstack compile',
185
+ validate: 'objectstack validate',
186
+ typecheck: 'tsc --noEmit',
187
+ },
188
+ dependencies: {
189
+ '@objectstack/spec': '^3.0.0',
190
+ '@objectstack/runtime': '^3.0.0',
191
+ '@objectstack/driver-memory': '^3.0.0',
192
+ '@objectstack/plugin-hono-server': '^3.0.0',
193
+ '@objectstack/plugin-auth': '^3.0.0',
194
+ },
195
+ devDependencies: {
196
+ '@objectstack/cli': '^3.0.0',
197
+ 'typescript': '^5.3.0',
198
+ },
199
+ }, null, 2) + '\n',
200
+ 'tsconfig.json': () => JSON.stringify({
201
+ compilerOptions: {
202
+ target: 'ES2022',
203
+ module: 'ESNext',
204
+ moduleResolution: 'bundler',
205
+ strict: true,
206
+ esModuleInterop: true,
207
+ skipLibCheck: true,
208
+ outDir: 'dist',
209
+ rootDir: '.',
210
+ declaration: true,
211
+ },
212
+ include: ['*.ts', 'src/**/*'],
213
+ exclude: ['dist', 'node_modules'],
214
+ }, null, 2) + '\n',
215
+ 'src/objects/contact.ts': () => `import { Data } from '@objectstack/spec';
216
+
217
+ const contact: Data.Object = {
218
+ name: 'contact',
219
+ label: 'Contact',
220
+ ownership: 'own',
221
+ fields: {
222
+ first_name: {
223
+ type: 'text',
224
+ label: 'First Name',
225
+ required: true,
226
+ },
227
+ last_name: {
228
+ type: 'text',
229
+ label: 'Last Name',
230
+ required: true,
231
+ },
232
+ email: {
233
+ type: 'text',
234
+ label: 'Email',
235
+ },
236
+ phone: {
237
+ type: 'text',
238
+ label: 'Phone',
239
+ },
240
+ company: {
241
+ type: 'lookup',
242
+ label: 'Company',
243
+ reference: 'company',
244
+ },
245
+ },
246
+ };
247
+
248
+ export default contact;
249
+ `,
250
+ 'src/objects/company.ts': () => `import { Data } from '@objectstack/spec';
251
+
252
+ const company: Data.Object = {
253
+ name: 'company',
254
+ label: 'Company',
255
+ ownership: 'own',
256
+ fields: {
257
+ name: {
258
+ type: 'text',
259
+ label: 'Company Name',
260
+ required: true,
261
+ },
262
+ website: {
263
+ type: 'text',
264
+ label: 'Website',
265
+ },
266
+ industry: {
267
+ type: 'select',
268
+ label: 'Industry',
269
+ options: [
270
+ { label: 'Technology', value: 'technology' },
271
+ { label: 'Finance', value: 'finance' },
272
+ { label: 'Healthcare', value: 'healthcare' },
273
+ { label: 'Other', value: 'other' },
274
+ ],
275
+ },
276
+ },
277
+ };
278
+
279
+ export default company;
280
+ `,
281
+ 'src/objects/deal.ts': () => `import { Data } from '@objectstack/spec';
282
+
283
+ const deal: Data.Object = {
284
+ name: 'deal',
285
+ label: 'Deal',
286
+ ownership: 'own',
287
+ fields: {
288
+ name: {
289
+ type: 'text',
290
+ label: 'Deal Name',
291
+ required: true,
292
+ },
293
+ amount: {
294
+ type: 'number',
295
+ label: 'Amount',
296
+ },
297
+ stage: {
298
+ type: 'select',
299
+ label: 'Stage',
300
+ options: [
301
+ { label: 'Prospecting', value: 'prospecting' },
302
+ { label: 'Qualification', value: 'qualification' },
303
+ { label: 'Proposal', value: 'proposal' },
304
+ { label: 'Closed Won', value: 'closed_won' },
305
+ { label: 'Closed Lost', value: 'closed_lost' },
306
+ ],
307
+ defaultValue: 'prospecting',
308
+ },
309
+ contact: {
310
+ type: 'lookup',
311
+ label: 'Contact',
312
+ reference: 'contact',
313
+ },
314
+ company: {
315
+ type: 'lookup',
316
+ label: 'Company',
317
+ reference: 'company',
318
+ },
319
+ close_date: {
320
+ type: 'date',
321
+ label: 'Close Date',
322
+ },
323
+ },
324
+ };
325
+
326
+ export default deal;
327
+ `,
328
+ 'src/objects/index.ts': () => `export { default as contact } from './contact';
329
+ export { default as company } from './company';
330
+ export { default as deal } from './deal';
331
+ `,
332
+ 'src/views/contact_list.ts': () => `import { UI } from '@objectstack/spec';
333
+
334
+ const contactList: UI.View = {
335
+ name: 'contact_list',
336
+ label: 'All Contacts',
337
+ object: 'contact',
338
+ type: 'list',
339
+ columns: ['first_name', 'last_name', 'email', 'phone', 'company'],
340
+ };
341
+
342
+ export default contactList;
343
+ `,
344
+ 'src/views/company_list.ts': () => `import { UI } from '@objectstack/spec';
345
+
346
+ const companyList: UI.View = {
347
+ name: 'company_list',
348
+ label: 'All Companies',
349
+ object: 'company',
350
+ type: 'list',
351
+ columns: ['name', 'website', 'industry'],
352
+ };
353
+
354
+ export default companyList;
355
+ `,
356
+ 'src/views/deal_list.ts': () => `import { UI } from '@objectstack/spec';
357
+
358
+ const dealList: UI.View = {
359
+ name: 'deal_list',
360
+ label: 'All Deals',
361
+ object: 'deal',
362
+ type: 'list',
363
+ columns: ['name', 'amount', 'stage', 'contact', 'close_date'],
364
+ };
365
+
366
+ export default dealList;
367
+ `,
368
+ 'src/apps/crm.ts': () => `import { UI } from '@objectstack/spec';
369
+
370
+ const crm: UI.App = {
371
+ name: 'crm',
372
+ label: 'CRM',
373
+ description: 'Customer Relationship Management',
374
+ navigation: [
375
+ { type: 'object', object: 'contact', label: 'Contacts' },
376
+ { type: 'object', object: 'company', label: 'Companies' },
377
+ { type: 'object', object: 'deal', label: 'Deals' },
378
+ ],
379
+ };
380
+
381
+ export default crm;
382
+ `,
383
+ 'src/apps/index.ts': () => `export { default as crm } from './crm';
384
+ `,
385
+ '.gitignore': () => `node_modules/
386
+ dist/
387
+ *.tsbuildinfo
388
+ `,
389
+ 'README.md': (name) => `# ${toTitleCase(name)}
390
+
391
+ A full-stack CRM application built with [ObjectStack](https://objectstack.com).
392
+
393
+ ## Quick Start
394
+
395
+ \`\`\`bash
396
+ npm install
397
+ npm run dev
398
+ \`\`\`
399
+
400
+ ## Project Structure
401
+
402
+ - \`objectstack.config.ts\` — Stack definition
403
+ - \`src/objects/\` — Data objects (Contact, Company, Deal)
404
+ - \`src/views/\` — List views
405
+ - \`src/apps/crm.ts\` — CRM app with navigation
406
+
407
+ ## Learn More
408
+
409
+ - [ObjectStack Documentation](https://objectstack.com/docs)
410
+ `,
411
+ },
412
+ },
413
+
414
+ plugin: {
415
+ description: 'Plugin skeleton with test setup',
416
+ files: {
417
+ 'objectstack.config.ts': (name) => `import { defineStack } from '@objectstack/spec';
418
+ import * as objects from './src/objects';
419
+
420
+ export default defineStack({
421
+ manifest: {
422
+ id: 'com.objectstack.plugin-${name}',
423
+ namespace: 'plugin_${name}',
424
+ version: '0.1.0',
425
+ type: 'plugin',
426
+ name: '${toTitleCase(name)} Plugin',
427
+ description: 'ObjectStack Plugin: ${toTitleCase(name)}',
428
+ },
429
+
430
+ objects: Object.values(objects),
431
+ });
432
+ `,
433
+ 'package.json': (name) => JSON.stringify({
434
+ name: `@objectstack/plugin-${name}`,
435
+ version: '0.1.0',
436
+ description: `ObjectStack Plugin: ${toTitleCase(name)}`,
437
+ main: 'dist/index.js',
438
+ types: 'dist/index.d.ts',
439
+ type: 'module',
440
+ scripts: {
441
+ build: 'tsc',
442
+ dev: 'tsc --watch',
443
+ test: 'vitest run',
444
+ validate: 'objectstack validate',
445
+ typecheck: 'tsc --noEmit',
446
+ },
447
+ keywords: ['objectstack', 'plugin', name],
448
+ author: '',
449
+ license: 'MIT',
450
+ dependencies: {
451
+ '@objectstack/spec': '^3.0.0',
452
+ },
453
+ devDependencies: {
454
+ '@types/node': '^22.0.0',
455
+ 'typescript': '^5.3.0',
456
+ 'vitest': '^4.0.0',
457
+ },
458
+ }, null, 2) + '\n',
459
+ 'tsconfig.json': () => JSON.stringify({
460
+ compilerOptions: {
461
+ target: 'ES2022',
462
+ module: 'ESNext',
463
+ moduleResolution: 'bundler',
464
+ strict: true,
465
+ esModuleInterop: true,
466
+ skipLibCheck: true,
467
+ outDir: 'dist',
468
+ rootDir: '.',
469
+ declaration: true,
470
+ },
471
+ include: ['*.ts', 'src/**/*'],
472
+ exclude: ['dist', 'node_modules'],
473
+ }, null, 2) + '\n',
474
+ 'src/index.ts': (name) => `/**
475
+ * ${toTitleCase(name)} Plugin for ObjectStack
476
+ *
477
+ * Entry point — re-exports all plugin metadata.
478
+ */
479
+ export * as objects from './objects';
480
+ `,
481
+ 'src/objects/sample.ts': (name) => `import { Data } from '@objectstack/spec';
482
+
483
+ const sample: Data.Object = {
484
+ name: '${name}_sample',
485
+ label: '${toTitleCase(name)} Sample',
486
+ ownership: 'own',
487
+ fields: {
488
+ name: {
489
+ type: 'text',
490
+ label: 'Name',
491
+ required: true,
492
+ },
493
+ },
494
+ };
495
+
496
+ export default sample;
497
+ `,
498
+ 'src/objects/index.ts': () => `export { default as sample } from './sample';
499
+ `,
500
+ 'test/sample.test.ts': (name) => `import { describe, it, expect } from 'vitest';
501
+ import sample from '../src/objects/sample';
502
+
503
+ describe('${name} plugin', () => {
504
+ it('should export a valid sample object', () => {
505
+ expect(sample).toBeDefined();
506
+ expect(sample.name).toBe('${name}_sample');
507
+ expect(sample.fields).toHaveProperty('name');
508
+ });
509
+ });
510
+ `,
511
+ '.gitignore': () => `node_modules/
512
+ dist/
513
+ *.tsbuildinfo
514
+ `,
515
+ 'README.md': (name) => `# @objectstack/plugin-${name}
516
+
517
+ ObjectStack Plugin: ${toTitleCase(name)}
518
+
519
+ ## Installation
520
+
521
+ \`\`\`bash
522
+ npm install @objectstack/plugin-${name}
523
+ \`\`\`
524
+
525
+ ## Usage
526
+
527
+ \`\`\`typescript
528
+ import { defineStack } from '@objectstack/spec';
529
+
530
+ export default defineStack({
531
+ plugins: [
532
+ '@objectstack/plugin-${name}',
533
+ ],
534
+ });
535
+ \`\`\`
536
+
537
+ ## Development
538
+
539
+ \`\`\`bash
540
+ # Run tests
541
+ npm test
542
+
543
+ # Build
544
+ npm run build
545
+
546
+ # Validate metadata
547
+ npm run validate
548
+ \`\`\`
549
+
550
+ ## License
551
+
552
+ MIT
553
+ `,
554
+ },
555
+ },
556
+ };
557
+
558
+ // ─── Helpers ────────────────────────────────────────────────────────
559
+
560
+ function toCamelCase(str: string): string {
561
+ return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
562
+ }
563
+
564
+ function toTitleCase(str: string): string {
565
+ return str.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
566
+ }
567
+
568
+ // ─── Formatting (matches @objectstack/cli style) ────────────────────
569
+
570
+ function printHeader(title: string) {
571
+ console.log(chalk.bold(`\n◆ ${title}`));
572
+ console.log(chalk.dim('─'.repeat(40)));
573
+ }
574
+
575
+ function printKV(key: string, value: string) {
576
+ console.log(` ${chalk.dim(key + ':')} ${chalk.white(value)}`);
577
+ }
578
+
579
+ function printSuccess(msg: string) {
580
+ console.log(chalk.green(` ✓ ${msg}`));
581
+ }
582
+
583
+ function printError(msg: string) {
584
+ console.log(chalk.red(` ✗ ${msg}`));
585
+ }
586
+
587
+ function printStep(msg: string) {
588
+ console.log(chalk.yellow(` → ${msg}`));
589
+ }
590
+
591
+ function printWarning(msg: string) {
592
+ console.log(chalk.yellow(` ⚠ ${msg}`));
593
+ }
594
+
595
+ // ─── CLI Program ────────────────────────────────────────────────────
596
+
597
+ const program = new Command()
598
+ .name('create-objectstack')
599
+ .description('Create a new ObjectStack project')
600
+ .version('3.0.0')
601
+ .argument('[name]', 'Project name (defaults to current directory name)')
602
+ .option(
603
+ '-t, --template <template>',
604
+ 'Project template: minimal-api, full-stack, plugin',
605
+ 'minimal-api',
606
+ )
607
+ .option('--skip-install', 'Skip dependency installation')
608
+ .action(async (name: string | undefined, options: { template: string; skipInstall?: boolean }) => {
609
+ // Banner
610
+ console.log('');
611
+ console.log(chalk.bold.cyan(' ╔═══════════════════════════════════╗'));
612
+ console.log(chalk.bold.cyan(' ║') + chalk.bold(' ◆ Create ObjectStack ') + chalk.dim('v3.0') + chalk.bold.cyan(' ║'));
613
+ console.log(chalk.bold.cyan(' ╚═══════════════════════════════════╝'));
614
+
615
+ printHeader('New Project');
616
+
617
+ // Resolve template
618
+ const template = TEMPLATES[options.template];
619
+ if (!template) {
620
+ printError(`Unknown template: ${options.template}`);
621
+ console.log(chalk.dim(` Available: ${Object.keys(TEMPLATES).join(', ')}`));
622
+ process.exit(1);
623
+ }
624
+
625
+ // Resolve project name and directory
626
+ const cwd = process.cwd();
627
+ const projectName = name || path.basename(cwd);
628
+ const targetDir = name ? path.resolve(cwd, name) : cwd;
629
+ const isCurrentDir = targetDir === cwd;
630
+
631
+ printKV('Project', projectName);
632
+ printKV('Template', `${options.template} — ${template.description}`);
633
+ printKV('Directory', targetDir);
634
+ console.log('');
635
+
636
+ // Guard: if creating in a sub-directory, check it doesn't already exist
637
+ if (!isCurrentDir && fs.existsSync(targetDir)) {
638
+ const existing = fs.readdirSync(targetDir);
639
+ if (existing.length > 0) {
640
+ printError(`Directory already exists and is not empty: ${targetDir}`);
641
+ process.exit(1);
642
+ }
643
+ }
644
+
645
+ const createdFiles: string[] = [];
646
+
647
+ try {
648
+ // Ensure target directory exists
649
+ if (!fs.existsSync(targetDir)) {
650
+ fs.mkdirSync(targetDir, { recursive: true });
651
+ }
652
+
653
+ // Write every file defined by the template
654
+ for (const [filePath, contentFn] of Object.entries(template.files)) {
655
+ const fullPath = path.join(targetDir, filePath);
656
+ const dir = path.dirname(fullPath);
657
+
658
+ if (!fs.existsSync(dir)) {
659
+ fs.mkdirSync(dir, { recursive: true });
660
+ }
661
+
662
+ fs.writeFileSync(fullPath, contentFn(projectName));
663
+ createdFiles.push(filePath);
664
+ }
665
+
666
+ // Summary
667
+ console.log(chalk.bold(' Created files:'));
668
+ for (const f of createdFiles) {
669
+ console.log(chalk.green(` + ${f}`));
670
+ }
671
+ console.log('');
672
+
673
+ // Install dependencies
674
+ if (!options.skipInstall) {
675
+ printStep('Installing dependencies...');
676
+ try {
677
+ // Detect package manager — prefer pnpm, fall back to npm
678
+ const pm = detectPackageManager();
679
+ execSync(`${pm} install`, { stdio: 'inherit', cwd: targetDir });
680
+ console.log('');
681
+ } catch {
682
+ printWarning('Dependency installation failed. Run `npm install` manually.');
683
+ console.log('');
684
+ }
685
+ }
686
+
687
+ printSuccess('Project created!');
688
+ console.log('');
689
+
690
+ // Next steps
691
+ console.log(chalk.bold(' Next steps:'));
692
+ if (!isCurrentDir) {
693
+ console.log(chalk.dim(` cd ${name}`));
694
+ }
695
+ if (options.skipInstall) {
696
+ console.log(chalk.dim(' npm install'));
697
+ }
698
+ console.log(chalk.dim(' npm run dev # Start development server'));
699
+ console.log(chalk.dim(' npm run validate # Check configuration'));
700
+ console.log('');
701
+
702
+ } catch (error: any) {
703
+ printError(error.message || String(error));
704
+ process.exit(1);
705
+ }
706
+ });
707
+
708
+ /**
709
+ * Detect available package manager (pnpm > npm).
710
+ */
711
+ function detectPackageManager(): string {
712
+ try {
713
+ execSync('pnpm --version', { stdio: 'ignore' });
714
+ return 'pnpm';
715
+ } catch {
716
+ return 'npm';
717
+ }
718
+ }
719
+
720
+ program.parse();