@apso/cli 0.27.0 → 0.29.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.
Files changed (41) hide show
  1. package/README.md +28 -1
  2. package/dist/commands/config.js +29 -0
  3. package/dist/commands/deploy.d.ts +1 -0
  4. package/dist/commands/deploy.js +50 -17
  5. package/dist/commands/dev.d.ts +9 -0
  6. package/dist/commands/dev.js +29 -2
  7. package/dist/commands/generate.js +26 -14
  8. package/dist/commands/github/connect.js +3 -1
  9. package/dist/commands/link.js +1 -0
  10. package/dist/commands/mcp/serve.d.ts +2 -0
  11. package/dist/commands/mcp/serve.js +40 -0
  12. package/dist/commands/update.js +1 -1
  13. package/dist/commands/use.js +2 -1
  14. package/dist/hooks/init/telemetry.js +6 -0
  15. package/dist/lib/api/services.d.ts +12 -0
  16. package/dist/lib/api/services.js +18 -6
  17. package/dist/lib/config/types.d.ts +10 -0
  18. package/dist/lib/config/types.js +1 -0
  19. package/dist/lib/deploy/server-push.d.ts +15 -0
  20. package/dist/lib/deploy/server-push.js +49 -0
  21. package/dist/lib/generators/base.d.ts +1 -0
  22. package/dist/lib/generators/go.d.ts +22 -1
  23. package/dist/lib/generators/go.js +47 -6
  24. package/dist/lib/generators/python.d.ts +18 -0
  25. package/dist/lib/generators/python.js +33 -0
  26. package/dist/lib/generators/typescript.js +3 -0
  27. package/dist/lib/git.js +1 -1
  28. package/dist/lib/migrate/sandbox.js +3 -1
  29. package/dist/lib/telemetry/telemetry.d.ts +20 -1
  30. package/dist/lib/telemetry/telemetry.js +68 -12
  31. package/dist/lib/templates/entities/entity.eta +1 -1
  32. package/dist/lib/templates/go/events/event-emitting-entities.eta +21 -0
  33. package/dist/lib/templates/go/index-module.eta +6 -6
  34. package/dist/lib/templates/python/events/event-emitting-entities.eta +22 -0
  35. package/dist/lib/types/entity.d.ts +6 -0
  36. package/dist/lib/types/generator.d.ts +1 -0
  37. package/dist/lib/utils/template.d.ts +10 -0
  38. package/dist/lib/utils/template.js +25 -1
  39. package/npm-shrinkwrap.json +23 -2
  40. package/oclif.manifest.json +8 -1
  41. package/package.json +3 -1
@@ -54,6 +54,7 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
54
54
  */
55
55
  abstract generateIndexModule(entities: Entity[], apiType: string, opts?: {
56
56
  emitEvents?: boolean;
57
+ http?: boolean;
57
58
  }): Promise<GeneratedFile[]>;
58
59
  /**
59
60
  * Generate guard files (auth, scope, etc.)
@@ -32,7 +32,28 @@ export declare class GoGenerator extends BaseGenerator {
32
32
  generateModule(_options: EntityGenerationOptions): Promise<GeneratedFile[]>;
33
33
  generateMigration(options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
34
34
  generateEnums(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
35
- generateIndexModule(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
35
+ generateIndexModule(entities: Entity[], _apiType: string, opts?: {
36
+ emitEvents?: boolean;
37
+ http?: boolean;
38
+ }): Promise<GeneratedFile[]>;
39
+ /**
40
+ * Emits a schema-derived manifest of the entities that opted in to
41
+ * domain-event emission (`.apsorc` `emitEvents`), mirroring the TypeScript
42
+ * generator. See apsoai/cli#82.
43
+ *
44
+ * Per the Apso Distribution Model the domain-event engine (the transactional
45
+ * GORM hook/callback, mapper, relay, delivery) is NOT generated here; it
46
+ * ships as a versioned library wired by the `domain-events` skill. The CLI's
47
+ * only job is to declare WHICH entities participate, via a single generated
48
+ * file: `events/event_emitting_entities.go`.
49
+ *
50
+ * Returns an empty array when no entity is opted in.
51
+ *
52
+ * Resolution mirrors TS: `entity.emitEvents ?? <top-level emitEvents> ?? false`.
53
+ */
54
+ generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
55
+ emitEvents?: boolean;
56
+ }): Promise<GeneratedFile[]>;
36
57
  generateGuards(_entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
37
58
  generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
38
59
  }
@@ -8,6 +8,7 @@ const base_1 = require("./base");
8
8
  const field_1 = require("../utils/field");
9
9
  const relationships_1 = require("../utils/relationships");
10
10
  const casing_1 = require("../utils/casing");
11
+ const events_1 = require("../events");
11
12
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
12
13
  /**
13
14
  * Go/Gin generator that produces GORM models, DTOs, Gin handlers,
@@ -336,12 +337,20 @@ ${downStatements}
336
337
  },
337
338
  ];
338
339
  }
339
- async generateIndexModule(entities, _apiType) {
340
- const entitiesData = entities.map((entity) => ({
341
- name: entity.name,
342
- camelName: (0, casing_1.camelCase)(entity.name),
343
- kebabName: (0, casing_1.kebabCase)(entity.name),
344
- }));
340
+ async generateIndexModule(entities, _apiType, opts) {
341
+ // Effective HTTP flag per entity (entity override > top-level default >
342
+ // true). When false we skip the handler + route registration but keep the
343
+ // service and model, matching the handler suppression in generate.ts.
344
+ // See apsoai/cli#95.
345
+ const entitiesData = entities.map((entity) => {
346
+ var _a, _b;
347
+ return ({
348
+ name: entity.name,
349
+ camelName: (0, casing_1.camelCase)(entity.name),
350
+ kebabName: (0, casing_1.kebabCase)(entity.name),
351
+ http: (_b = (_a = entity.http) !== null && _a !== void 0 ? _a : opts === null || opts === void 0 ? void 0 : opts.http) !== null && _b !== void 0 ? _b : true,
352
+ });
353
+ });
345
354
  const content = await this.renderTemplate("./index-module", {
346
355
  entities: entitiesData,
347
356
  });
@@ -370,6 +379,38 @@ ${modelRefs}
370
379
  },
371
380
  ];
372
381
  }
382
+ /**
383
+ * Emits a schema-derived manifest of the entities that opted in to
384
+ * domain-event emission (`.apsorc` `emitEvents`), mirroring the TypeScript
385
+ * generator. See apsoai/cli#82.
386
+ *
387
+ * Per the Apso Distribution Model the domain-event engine (the transactional
388
+ * GORM hook/callback, mapper, relay, delivery) is NOT generated here; it
389
+ * ships as a versioned library wired by the `domain-events` skill. The CLI's
390
+ * only job is to declare WHICH entities participate, via a single generated
391
+ * file: `events/event_emitting_entities.go`.
392
+ *
393
+ * Returns an empty array when no entity is opted in.
394
+ *
395
+ * Resolution mirrors TS: `entity.emitEvents ?? <top-level emitEvents> ?? false`.
396
+ */
397
+ async generateDomainEvents(entities, _apiType, opts) {
398
+ const emittingEntities = (0, events_1.getEventEmittingEntities)(entities, opts === null || opts === void 0 ? void 0 : opts.emitEvents);
399
+ if (emittingEntities.length === 0) {
400
+ return [];
401
+ }
402
+ const content = await this.renderTemplate("./events/event-emitting-entities", {
403
+ emittingEntities: emittingEntities.map((entity) => ({
404
+ name: entity.name,
405
+ })),
406
+ });
407
+ return [
408
+ {
409
+ path: "events/event_emitting_entities.go",
410
+ content,
411
+ },
412
+ ];
413
+ }
373
414
  async generateGuards(_entities, auth) {
374
415
  const content = await this.renderTemplate("./middleware/auth", {
375
416
  auth,
@@ -17,6 +17,24 @@ export declare class PythonGenerator extends BaseGenerator {
17
17
  generateMigration(options: MigrationGenerationOptions): Promise<GeneratedFile[]>;
18
18
  generateEnums(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
19
19
  generateIndexModule(entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
20
+ /**
21
+ * Emits a schema-derived manifest of the entities that opted in to
22
+ * domain-event emission (`.apsorc` `emitEvents`), mirroring the TypeScript
23
+ * generator. See apsoai/cli#81.
24
+ *
25
+ * Per the Apso Distribution Model the domain-event engine (the transactional
26
+ * SQLAlchemy session hook, mapper, relay, delivery) is NOT generated here; it
27
+ * ships as a versioned library wired by the `domain-events` skill. The CLI's
28
+ * only job is to declare WHICH entities participate, via a single generated
29
+ * file: `events/event_emitting_entities.py`.
30
+ *
31
+ * Returns an empty array when no entity is opted in.
32
+ *
33
+ * Resolution mirrors TS: `entity.emitEvents ?? <top-level emitEvents> ?? false`.
34
+ */
35
+ generateDomainEvents(entities: Entity[], _apiType?: string, opts?: {
36
+ emitEvents?: boolean;
37
+ }): Promise<GeneratedFile[]>;
20
38
  generateGuards(_entities: Entity[], _auth?: AuthConfig): Promise<GeneratedFile[]>;
21
39
  generateQueryUtils(_entities: Entity[], _apiType: string): Promise<GeneratedFile[]>;
22
40
  }
@@ -8,6 +8,7 @@ const base_1 = require("./base");
8
8
  const field_1 = require("../utils/field");
9
9
  const relationships_1 = require("../utils/relationships");
10
10
  const casing_1 = require("../utils/casing");
11
+ const events_1 = require("../events");
11
12
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
12
13
  /**
13
14
  * Python/FastAPI generator that produces SQLAlchemy models, Pydantic schemas,
@@ -281,6 +282,38 @@ ${downStatements || " pass"}
281
282
  },
282
283
  ];
283
284
  }
285
+ /**
286
+ * Emits a schema-derived manifest of the entities that opted in to
287
+ * domain-event emission (`.apsorc` `emitEvents`), mirroring the TypeScript
288
+ * generator. See apsoai/cli#81.
289
+ *
290
+ * Per the Apso Distribution Model the domain-event engine (the transactional
291
+ * SQLAlchemy session hook, mapper, relay, delivery) is NOT generated here; it
292
+ * ships as a versioned library wired by the `domain-events` skill. The CLI's
293
+ * only job is to declare WHICH entities participate, via a single generated
294
+ * file: `events/event_emitting_entities.py`.
295
+ *
296
+ * Returns an empty array when no entity is opted in.
297
+ *
298
+ * Resolution mirrors TS: `entity.emitEvents ?? <top-level emitEvents> ?? false`.
299
+ */
300
+ async generateDomainEvents(entities, _apiType, opts) {
301
+ const emittingEntities = (0, events_1.getEventEmittingEntities)(entities, opts === null || opts === void 0 ? void 0 : opts.emitEvents);
302
+ if (emittingEntities.length === 0) {
303
+ return [];
304
+ }
305
+ const content = await this.renderTemplate("./events/event-emitting-entities", {
306
+ emittingEntities: emittingEntities.map((entity) => ({
307
+ name: entity.name,
308
+ })),
309
+ });
310
+ return [
311
+ {
312
+ path: "events/event_emitting_entities.py",
313
+ content,
314
+ },
315
+ ];
316
+ }
284
317
  async generateGuards(_entities, _auth) {
285
318
  // Python auth/guards would be implemented differently (e.g., FastAPI dependencies)
286
319
  // This is a placeholder for future implementation
@@ -85,6 +85,9 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
85
85
  createPrimaryKey,
86
86
  primaryKeyType,
87
87
  snakeCasedName: (0, casing_1.snakeCase)(name),
88
+ // Honor an explicit per-entity `table` override; otherwise derive from
89
+ // the entity name. See apsoai/cli#98.
90
+ tableName: entity.table || (0, casing_1.snakeCase)(name),
88
91
  createdAt,
89
92
  updatedAt,
90
93
  pluralizedName: (0, casing_1.camelCase)((0, pluralize_1.default)(name)),
package/dist/lib/git.js CHANGED
@@ -55,7 +55,7 @@ exports.getOriginUrl = getOriginUrl;
55
55
  function parseRepoFullName(url) {
56
56
  if (!url)
57
57
  return null;
58
- const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/i);
58
+ const m = url.match(/github\.com[/:]([^/]+)\/(.+?)(?:\.git)?\/?$/i);
59
59
  return m ? `${m[1]}/${m[2]}` : null;
60
60
  }
61
61
  exports.parseRepoFullName = parseRepoFullName;
@@ -67,7 +67,9 @@ function buildEntityClasses(entities, relationshipMap = {}) {
67
67
  const classMap = new Map();
68
68
  for (const entityDef of entities) {
69
69
  const className = entityDef.name;
70
- const tableName = (0, casing_1.snakeCase)(entityDef.name);
70
+ // Honor an explicit per-entity `table` override so local migration tests
71
+ // match the generated entity's table name. See apsoai/cli#98.
72
+ const tableName = entityDef.table || (0, casing_1.snakeCase)(entityDef.name);
71
73
  const EntityClass = { [className]: class {
72
74
  } }[className];
73
75
  typeorm.Entity(tableName)(EntityClass);
@@ -1,11 +1,29 @@
1
1
  declare function isDisabled(telemetryDisabled: boolean): boolean;
2
- /** Stable, non-PII fallback id for logged-out users (no username, no MAC). */
2
+ /** Stable, non-PII fallback id (only used if the config write fails). */
3
3
  declare function anonMachineId(): string;
4
+ /**
5
+ * Read (or lazily create + persist) the anonymous install id: a random UUID
6
+ * stored in the CLI config. Not derived from any machine identifier. Falls
7
+ * back to a stable hashed id only if the config can't be written.
8
+ */
9
+ declare function getOrCreateInstallId(currentInstallId?: string): string;
4
10
  /**
5
11
  * Initialize telemetry. Call once at process start (init hook). Safe to call
6
12
  * when disabled — it just no-ops everything downstream.
7
13
  */
8
14
  export declare function initTelemetry(): void;
15
+ /**
16
+ * The one-time transparency notice text (issue #96). Printed to stderr by the
17
+ * init hook so it never pollutes stdout / piped output.
18
+ */
19
+ export declare const FIRST_RUN_NOTICE: string;
20
+ /**
21
+ * True once per install: telemetry is enabled and the notice hasn't been shown
22
+ * yet. The init hook prints the notice and then calls markFirstRunNoticeShown().
23
+ */
24
+ export declare function shouldShowFirstRunNotice(): boolean;
25
+ /** Persist that the transparency notice has been shown. Best-effort. */
26
+ export declare function markFirstRunNoticeShown(): void;
9
27
  /** Emit a PostHog event. No-op when disabled. */
10
28
  export declare function track(event: string, properties?: Record<string, unknown>): void;
11
29
  /** Report an exception to Sentry. No-op when disabled. */
@@ -17,5 +35,6 @@ export declare function shutdownTelemetry(): Promise<void>;
17
35
  export declare const __testing: {
18
36
  isDisabled: typeof isDisabled;
19
37
  anonMachineId: typeof anonMachineId;
38
+ getOrCreateInstallId: typeof getOrCreateInstallId;
20
39
  };
21
40
  export {};
@@ -1,15 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.__testing = exports.shutdownTelemetry = exports.elapsedMs = exports.captureException = exports.track = exports.initTelemetry = void 0;
3
+ exports.__testing = exports.shutdownTelemetry = exports.elapsedMs = exports.captureException = exports.track = exports.markFirstRunNoticeShown = exports.shouldShowFirstRunNotice = exports.FIRST_RUN_NOTICE = exports.initTelemetry = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  /**
6
6
  * CLI telemetry — usage analytics (PostHog) + error reporting (Sentry).
7
7
  *
8
8
  * Wired to the same Apso systems the web app and build engine use so CLI usage
9
9
  * shows up in the same funnels. Fully opt-out:
10
- * - `apso config set telemetryDisabled true`
11
- * - env DO_NOT_TRACK=1 (honors the console.dev standard)
12
- * - env APSO_TELEMETRY_DISABLED=1
10
+ * - `apso config set telemetry off` (or `telemetryDisabled true`)
11
+ * - env APSO_TELEMETRY=0
12
+ * - env DO_NOT_TRACK=1 (honors the consoledonottrack.com standard)
13
+ * - env APSO_TELEMETRY_DISABLED=1 (legacy alias)
14
+ *
15
+ * The anonymous id is a random UUID persisted in the CLI config (`installId`),
16
+ * never derived from any machine identifier. No code, schema, or file contents
17
+ * are ever collected.
13
18
  *
14
19
  * Everything here is best-effort and MUST NOT throw or block a command. All
15
20
  * public functions swallow their own errors. The PostHog project keys and the
@@ -38,27 +43,49 @@ catch {
38
43
  let posthog = null;
39
44
  let enabled = false;
40
45
  let sentryOn = false;
46
+ let authenticatedUser = false;
41
47
  let distinctId = "cli-anonymous";
42
48
  let environment = "production";
43
49
  let startedAt = Date.now();
50
+ let noticePending = false;
44
51
  function isDisabled(telemetryDisabled) {
45
52
  if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true")
46
53
  return true;
54
+ // Canonical opt-out env var (issue #96); APSO_TELEMETRY_DISABLED is a legacy alias.
55
+ if (process.env.APSO_TELEMETRY === "0" || process.env.APSO_TELEMETRY === "false")
56
+ return true;
47
57
  if (process.env.APSO_TELEMETRY_DISABLED === "1")
48
58
  return true;
49
- return !!telemetryDisabled;
59
+ return Boolean(telemetryDisabled);
50
60
  }
51
- /** Stable, non-PII fallback id for logged-out users (no username, no MAC). */
61
+ /** Stable, non-PII fallback id (only used if the config write fails). */
52
62
  function anonMachineId() {
53
63
  const seed = `${os_1.default.hostname()}|${os_1.default.platform()}|${os_1.default.arch()}`;
54
64
  return "cli_" + (0, crypto_1.createHash)("sha256").update(seed).digest("hex").slice(0, 20);
55
65
  }
66
+ /**
67
+ * Read (or lazily create + persist) the anonymous install id: a random UUID
68
+ * stored in the CLI config. Not derived from any machine identifier. Falls
69
+ * back to a stable hashed id only if the config can't be written.
70
+ */
71
+ function getOrCreateInstallId(currentInstallId) {
72
+ if (currentInstallId)
73
+ return currentInstallId;
74
+ const id = (0, crypto_1.randomUUID)();
75
+ try {
76
+ config_1.globalConfig.write({ installId: id });
77
+ return id;
78
+ }
79
+ catch {
80
+ return anonMachineId();
81
+ }
82
+ }
56
83
  /**
57
84
  * Initialize telemetry. Call once at process start (init hook). Safe to call
58
85
  * when disabled — it just no-ops everything downstream.
59
86
  */
60
87
  function initTelemetry() {
61
- var _a, _b, _c;
88
+ var _a, _b, _c, _d;
62
89
  try {
63
90
  startedAt = Date.now();
64
91
  const cfg = config_1.globalConfig.read();
@@ -70,10 +97,13 @@ function initTelemetry() {
70
97
  environment = (cfg.apiUrl || "").includes("staging")
71
98
  ? "staging"
72
99
  : "production";
100
+ // Show the transparency notice once, on the first enabled run.
101
+ noticePending = !cfg.telemetryNoticeShown;
73
102
  const creds = config_1.credentials.read();
74
- distinctId = ((_a = creds === null || creds === void 0 ? void 0 : creds.user) === null || _a === void 0 ? void 0 : _a.id) || anonMachineId();
103
+ authenticatedUser = Boolean((_a = creds === null || creds === void 0 ? void 0 : creds.user) === null || _a === void 0 ? void 0 : _a.id);
104
+ distinctId = ((_b = creds === null || creds === void 0 ? void 0 : creds.user) === null || _b === void 0 ? void 0 : _b.id) || getOrCreateInstallId(cfg.installId);
75
105
  posthog = new posthog_node_1.PostHog(environment === "staging" ? POSTHOG_KEY_STAGING : POSTHOG_KEY_PROD, { host: POSTHOG_HOST, flushAt: 1, flushInterval: 0 });
76
- if ((_b = creds === null || creds === void 0 ? void 0 : creds.user) === null || _b === void 0 ? void 0 : _b.email) {
106
+ if ((_c = creds === null || creds === void 0 ? void 0 : creds.user) === null || _c === void 0 ? void 0 : _c.email) {
77
107
  // Attach identity so CLI events attribute to the same person as the app.
78
108
  posthog.identify({
79
109
  distinctId,
@@ -88,7 +118,7 @@ function initTelemetry() {
88
118
  release: `apso-cli@${cliVersion}`,
89
119
  tracesSampleRate: 0,
90
120
  });
91
- Sentry.setUser({ id: distinctId, email: (_c = creds === null || creds === void 0 ? void 0 : creds.user) === null || _c === void 0 ? void 0 : _c.email });
121
+ Sentry.setUser({ id: distinctId, email: (_d = creds === null || creds === void 0 ? void 0 : creds.user) === null || _d === void 0 ? void 0 : _d.email });
92
122
  Sentry.setTag("cli_version", cliVersion);
93
123
  sentryOn = true;
94
124
  }
@@ -105,9 +135,35 @@ function commonProps() {
105
135
  os: os_1.default.platform(),
106
136
  arch: os_1.default.arch(),
107
137
  node_version: process.version,
108
- authenticated: distinctId !== anonMachineId() && !distinctId.startsWith("cli_"),
138
+ authenticated: authenticatedUser,
109
139
  };
110
140
  }
141
+ /**
142
+ * The one-time transparency notice text (issue #96). Printed to stderr by the
143
+ * init hook so it never pollutes stdout / piped output.
144
+ */
145
+ exports.FIRST_RUN_NOTICE = "Apso collects anonymous usage data (command name, CLI version, OS) to improve the tool.\n" +
146
+ "No code, schema, file contents, or personal data is collected.\n" +
147
+ "Opt out any time: apso config set telemetry off (or APSO_TELEMETRY=0, or DO_NOT_TRACK=1)";
148
+ /**
149
+ * True once per install: telemetry is enabled and the notice hasn't been shown
150
+ * yet. The init hook prints the notice and then calls markFirstRunNoticeShown().
151
+ */
152
+ function shouldShowFirstRunNotice() {
153
+ return enabled && noticePending;
154
+ }
155
+ exports.shouldShowFirstRunNotice = shouldShowFirstRunNotice;
156
+ /** Persist that the transparency notice has been shown. Best-effort. */
157
+ function markFirstRunNoticeShown() {
158
+ noticePending = false;
159
+ try {
160
+ config_1.globalConfig.write({ telemetryNoticeShown: true });
161
+ }
162
+ catch {
163
+ // ignore — worst case the notice shows again next run
164
+ }
165
+ }
166
+ exports.markFirstRunNoticeShown = markFirstRunNoticeShown;
111
167
  /** Emit a PostHog event. No-op when disabled. */
112
168
  function track(event, properties = {}) {
113
169
  try {
@@ -159,4 +215,4 @@ async function shutdownTelemetry() {
159
215
  }
160
216
  }
161
217
  exports.shutdownTelemetry = shutdownTelemetry;
162
- exports.__testing = { isDisabled, anonMachineId };
218
+ exports.__testing = { isDisabled, anonMachineId, getOrCreateInstallId };
@@ -47,7 +47,7 @@ import {<%= assocName %>} from '../<%= assocName %>/<%= assocName %>.entity';
47
47
 
48
48
  const { CREATE, UPDATE } = CrudValidationGroups;
49
49
 
50
- @Entity('<%= it.snakeCasedName %>')
50
+ @Entity('<%= it.tableName %>')
51
51
  <% it.indexes.forEach((index) => { %>
52
52
  @Index([<%~ index.fields.map((field) => `"${field}"`).join(', ') %>]<% if (index.unique) { %>, { unique: true } <% } %>)
53
53
  <% }) %>
@@ -0,0 +1,21 @@
1
+ <%~ includeFile('../header.eta') %>
2
+ package events
3
+
4
+ import "app/autogen/models"
5
+
6
+ // Schema-derived manifest of entities that opted in to domain-event emission
7
+ // (.apsorc `emitEvents`). The domain-event engine itself (the transactional
8
+ // GORM hook/callback, mapper, relay, and delivery) is NOT generated here; it
9
+ // ships as a versioned library wired by the `domain-events` skill. This file
10
+ // only declares WHICH entities participate, mirroring the TypeScript manifest.
11
+
12
+ // EventEmittingModels are the opted-in model instances (for the engine to
13
+ // register GORM hooks/callbacks against).
14
+ var EventEmittingModels = []interface{}{
15
+ <% it.emittingEntities.forEach((entity) => { %> models.<%= entity.name %>{},
16
+ <% }) %>}
17
+
18
+ // EventEmittingEntityNames are the opted-in entity names.
19
+ var EventEmittingEntityNames = []string{
20
+ <% it.emittingEntities.forEach((entity) => { %> "<%= entity.name %>",
21
+ <% }) %>}
@@ -1,17 +1,17 @@
1
1
  <%~ includeFile('./header.eta', it) %>
2
-
2
+ <% const httpEntities = it.entities.filter((entity) => entity.http); %>
3
3
  package routes
4
4
 
5
5
  import (
6
- "app/autogen/handlers"
7
- "app/autogen/services"
6
+ <% if (httpEntities.length > 0) { %> "app/autogen/handlers"
7
+ <% } %> "app/autogen/services"
8
8
  "github.com/gin-gonic/gin"
9
9
  "gorm.io/gorm"
10
10
  )
11
11
 
12
12
  // RegisterAllRoutes registers all entity routes
13
13
  func RegisterAllRoutes(r *gin.RouterGroup, db *gorm.DB) {
14
- <% it.entities.forEach((entity) => { %>
14
+ <% httpEntities.forEach((entity) => { %>
15
15
  // <%= entity.name %> routes
16
16
  <%= entity.camelName %>Service := services.New<%= entity.name %>Service(db)
17
17
  <%= entity.camelName %>Handler := handlers.New<%= entity.name %>Handler(<%= entity.camelName %>Service)
@@ -38,7 +38,7 @@ func NewServices(db *gorm.DB) *Services {
38
38
 
39
39
  // Handlers holds all handler instances
40
40
  type Handlers struct {
41
- <% it.entities.forEach((entity) => { %>
41
+ <% httpEntities.forEach((entity) => { %>
42
42
  <%= entity.name %>Handler *handlers.<%= entity.name %>Handler
43
43
  <% }) %>
44
44
  }
@@ -46,7 +46,7 @@ type Handlers struct {
46
46
  // NewHandlers creates all handler instances
47
47
  func NewHandlers(s *Services) *Handlers {
48
48
  return &Handlers{
49
- <% it.entities.forEach((entity) => { %>
49
+ <% httpEntities.forEach((entity) => { %>
50
50
  <%= entity.name %>Handler: handlers.New<%= entity.name %>Handler(s.<%= entity.name %>Service),
51
51
  <% }) %>
52
52
  }
@@ -0,0 +1,22 @@
1
+ <%~ includeFile('../header.eta') %>
2
+ <% it.emittingEntities.forEach((entity) => { %>
3
+ from ..models.<%= entity.name.toLowerCase() %> import <%= entity.name %>
4
+ <% }) %>
5
+
6
+ # Schema-derived manifest of entities that opted in to domain-event emission
7
+ # (`.apsorc` `emitEvents`). The domain-event engine itself (the transactional
8
+ # SQLAlchemy session hook, mapper, relay, and delivery) is NOT generated here;
9
+ # it ships as a versioned library wired by the `domain-events` skill. This file
10
+ # only declares WHICH entities participate, mirroring the TypeScript manifest.
11
+
12
+ EVENT_EMITTING_ENTITIES = [
13
+ <% it.emittingEntities.forEach((entity) => { %>
14
+ <%= entity.name %>,
15
+ <% }) %>
16
+ ]
17
+
18
+ EVENT_EMITTING_ENTITY_NAMES = [
19
+ <% it.emittingEntities.forEach((entity) => { %>
20
+ "<%= entity.name %>",
21
+ <% }) %>
22
+ ]
@@ -26,6 +26,12 @@ export interface ScopeOptions {
26
26
  }
27
27
  export interface Entity {
28
28
  name: string;
29
+ /**
30
+ * Explicit database table name. When set, it overrides the default table
31
+ * name derived from `name` (snake_case). Use it to avoid SQL reserved words
32
+ * (e.g. an entity `Order` deriving to the reserved word `order`).
33
+ */
34
+ table?: string;
29
35
  created_at?: boolean;
30
36
  updated_at?: boolean;
31
37
  primaryKeyType?: "serial" | "uuid";
@@ -246,6 +246,7 @@ export interface LanguageGenerator {
246
246
  */
247
247
  generateIndexModule(entities: Entity[], apiType: string, opts?: {
248
248
  emitEvents?: boolean;
249
+ http?: boolean;
249
250
  }): Promise<GeneratedFile[]>;
250
251
  /**
251
252
  * Generate guard files (auth, scope, etc.)
@@ -6,6 +6,16 @@ export declare const PROJECT_NAME_PATTERN: RegExp;
6
6
  * Removes the .git directory after cloning.
7
7
  */
8
8
  export declare function cloneTemplate(projectPath: string, language: TargetLanguage, log: (msg: string) => void): void;
9
+ /**
10
+ * Ensure the scaffolded project has a `.env` file.
11
+ *
12
+ * Templates ship `.env.local` (and `.env.example`) but the NestJS env loader
13
+ * reads `.env` by default, so a fresh scaffold would silently ignore the
14
+ * shipped values and run on built-in defaults (wrong port, wrong database).
15
+ * Copy `.env.local` (preferred) or `.env.example` to `.env` when no `.env`
16
+ * exists yet. See apsoai/cli#103.
17
+ */
18
+ export declare function ensureEnvFile(projectPath: string, log: (msg: string) => void): void;
9
19
  /**
10
20
  * Initialize a fresh git repository in the given directory.
11
21
  */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.initGitRepo = exports.cloneTemplate = exports.PROJECT_NAME_PATTERN = exports.TEMPLATE_REPOS = void 0;
3
+ exports.initGitRepo = exports.ensureEnvFile = exports.cloneTemplate = exports.PROJECT_NAME_PATTERN = exports.TEMPLATE_REPOS = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs = tslib_1.__importStar(require("fs"));
6
6
  const path = tslib_1.__importStar(require("path"));
@@ -35,8 +35,32 @@ function cloneTemplate(projectPath, language, log) {
35
35
  `Please check your network connection and ensure the repository exists at ${repoUrl}`);
36
36
  }
37
37
  shelljs_1.default.rm("-rf", path.join(projectPath, ".git"));
38
+ ensureEnvFile(projectPath, log);
38
39
  }
39
40
  exports.cloneTemplate = cloneTemplate;
41
+ /**
42
+ * Ensure the scaffolded project has a `.env` file.
43
+ *
44
+ * Templates ship `.env.local` (and `.env.example`) but the NestJS env loader
45
+ * reads `.env` by default, so a fresh scaffold would silently ignore the
46
+ * shipped values and run on built-in defaults (wrong port, wrong database).
47
+ * Copy `.env.local` (preferred) or `.env.example` to `.env` when no `.env`
48
+ * exists yet. See apsoai/cli#103.
49
+ */
50
+ function ensureEnvFile(projectPath, log) {
51
+ const envPath = path.join(projectPath, ".env");
52
+ if (fs.existsSync(envPath))
53
+ return;
54
+ for (const source of [".env.local", ".env.example"]) {
55
+ const sourcePath = path.join(projectPath, source);
56
+ if (fs.existsSync(sourcePath)) {
57
+ fs.copyFileSync(sourcePath, envPath);
58
+ log(`Created .env from ${source}`);
59
+ return;
60
+ }
61
+ }
62
+ }
63
+ exports.ensureEnvFile = ensureEnvFile;
40
64
  /**
41
65
  * Initialize a fresh git repository in the given directory.
42
66
  */
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.27.0",
9
+ "version": "0.29.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@biomejs/biome": "^1.9.4",
@@ -17,6 +17,7 @@
17
17
  "@oclif/plugin-plugins": "^3",
18
18
  "@oclif/plugin-warn-if-update-available": "^2.1.1",
19
19
  "@sentry/node": "^10.68.0",
20
+ "adm-zip": "^0.6.0",
20
21
  "debug": "^4.3.4",
21
22
  "eta": "^2.0.0",
22
23
  "inquirer": "^8.2.7",
@@ -35,6 +36,7 @@
35
36
  "devDependencies": {
36
37
  "@jest/globals": "^29.5.0",
37
38
  "@oclif/test": "^2.4.7",
39
+ "@types/adm-zip": "^0.5.8",
38
40
  "@types/chai": "^4",
39
41
  "@types/inquirer": "^8.2.12",
40
42
  "@types/mocha": "^10",
@@ -2659,6 +2661,16 @@
2659
2661
  "url": "https://github.com/sponsors/isaacs"
2660
2662
  }
2661
2663
  },
2664
+ "node_modules/@types/adm-zip": {
2665
+ "version": "0.5.8",
2666
+ "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz",
2667
+ "integrity": "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==",
2668
+ "dev": true,
2669
+ "license": "MIT",
2670
+ "dependencies": {
2671
+ "@types/node": "*"
2672
+ }
2673
+ },
2662
2674
  "node_modules/@types/babel__core": {
2663
2675
  "version": "7.20.1",
2664
2676
  "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz",
@@ -3556,6 +3568,15 @@
3556
3568
  "node": ">=0.4.0"
3557
3569
  }
3558
3570
  },
3571
+ "node_modules/adm-zip": {
3572
+ "version": "0.6.0",
3573
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
3574
+ "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
3575
+ "license": "MIT",
3576
+ "engines": {
3577
+ "node": ">=14.0"
3578
+ }
3579
+ },
3559
3580
  "node_modules/agent-base": {
3560
3581
  "version": "6.0.2",
3561
3582
  "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",