@apso/cli 0.28.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.
- package/README.md +28 -1
- package/dist/commands/config.js +29 -0
- package/dist/commands/dev.d.ts +9 -0
- package/dist/commands/dev.js +29 -2
- package/dist/commands/generate.js +26 -14
- package/dist/commands/github/connect.js +3 -1
- package/dist/commands/link.js +1 -0
- package/dist/commands/update.js +1 -1
- package/dist/commands/use.js +2 -1
- package/dist/hooks/init/telemetry.js +6 -0
- package/dist/lib/api/services.js +7 -5
- package/dist/lib/config/types.d.ts +10 -0
- package/dist/lib/config/types.js +1 -0
- package/dist/lib/generators/base.d.ts +1 -0
- package/dist/lib/generators/go.d.ts +22 -1
- package/dist/lib/generators/go.js +47 -6
- package/dist/lib/generators/python.d.ts +18 -0
- package/dist/lib/generators/python.js +33 -0
- package/dist/lib/generators/typescript.js +3 -0
- package/dist/lib/git.js +1 -1
- package/dist/lib/migrate/sandbox.js +3 -1
- package/dist/lib/telemetry/telemetry.d.ts +20 -1
- package/dist/lib/telemetry/telemetry.js +68 -12
- package/dist/lib/templates/entities/entity.eta +1 -1
- package/dist/lib/templates/go/events/event-emitting-entities.eta +21 -0
- package/dist/lib/templates/go/index-module.eta +6 -6
- package/dist/lib/templates/python/events/event-emitting-entities.eta +22 -0
- package/dist/lib/types/entity.d.ts +6 -0
- package/dist/lib/types/generator.d.ts +1 -0
- package/dist/lib/utils/template.d.ts +10 -0
- package/dist/lib/utils/template.js +25 -1
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -314,7 +314,7 @@ apso config reset # Reset to defaults
|
|
|
314
314
|
| `webUrl` | string | Platform web URL |
|
|
315
315
|
| `verbose` | boolean | Enable verbose output |
|
|
316
316
|
| `noColor` | boolean | Disable colored output |
|
|
317
|
-
| `telemetryDisabled` | boolean | Opt out of anonymous telemetry |
|
|
317
|
+
| `telemetryDisabled` | boolean | Opt out of anonymous telemetry (or `apso config set telemetry off`) |
|
|
318
318
|
| `defaultWorkspace` | string | Default workspace slug |
|
|
319
319
|
|
|
320
320
|
Boolean values accept `true`/`false` or `1`/`0`.
|
|
@@ -327,6 +327,7 @@ Environment variables override config file values:
|
|
|
327
327
|
| `APSO_WEB_URL` | `webUrl` |
|
|
328
328
|
| `APSO_DEBUG=true` | `verbose` |
|
|
329
329
|
| `NO_COLOR` or `APSO_NO_COLOR=true` | `noColor` |
|
|
330
|
+
| `APSO_TELEMETRY=0` / `DO_NOT_TRACK=1` | `telemetryDisabled` |
|
|
330
331
|
|
|
331
332
|
### `apso schema`
|
|
332
333
|
|
|
@@ -348,6 +349,32 @@ apso [command] --help # Show command help
|
|
|
348
349
|
apso [command] --version # Show CLI version
|
|
349
350
|
```
|
|
350
351
|
|
|
352
|
+
## Telemetry & privacy
|
|
353
|
+
|
|
354
|
+
The CLI sends anonymous usage data so we can see which commands are used and
|
|
355
|
+
prioritize accordingly. It is opt-out and carries no personal data.
|
|
356
|
+
|
|
357
|
+
**What is collected:** command name (`generate`, `init`, `deploy`, …), CLI
|
|
358
|
+
version, OS and architecture, Node version, command duration and success, and
|
|
359
|
+
an anonymous install id (a random UUID stored in your CLI config — not derived
|
|
360
|
+
from any machine identifier). If you are logged in, events attribute to your
|
|
361
|
+
Apso account, the same as the web app.
|
|
362
|
+
|
|
363
|
+
**What is never collected:** your source code, your `.apsorc` or schema
|
|
364
|
+
contents, file paths, environment variable values, or any other file contents.
|
|
365
|
+
|
|
366
|
+
**Opt out** any of these ways:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
apso config set telemetry off # persisted in your CLI config
|
|
370
|
+
export APSO_TELEMETRY=0 # per-shell
|
|
371
|
+
export DO_NOT_TRACK=1 # honors the consoledonottrack.com standard
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
On first run the CLI prints a one-time notice pointing at these opt-outs.
|
|
375
|
+
See [`docs/telemetry.md`](docs/telemetry.md) for full detail. Aggregate
|
|
376
|
+
public download stats (npm/PyPI) are available via `node scripts/usage-stats.mjs`.
|
|
377
|
+
|
|
351
378
|
## Supported languages
|
|
352
379
|
|
|
353
380
|
| Language | Framework | ORM | Status |
|
package/dist/commands/config.js
CHANGED
|
@@ -26,6 +26,19 @@ function parseBoolean(value) {
|
|
|
26
26
|
return false;
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse the friendly `telemetry on|off` value into the underlying
|
|
31
|
+
* `telemetryDisabled` boolean. `off` (opt out) => disabled = true.
|
|
32
|
+
* Accepts on/off, enabled/disabled, and true/false/1/0.
|
|
33
|
+
*/
|
|
34
|
+
function parseTelemetryToggle(value) {
|
|
35
|
+
const lower = value.toLowerCase();
|
|
36
|
+
if (["off", "false", "0", "disabled", "no"].includes(lower))
|
|
37
|
+
return true;
|
|
38
|
+
if (["on", "true", "1", "enabled", "yes"].includes(lower))
|
|
39
|
+
return false;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
29
42
|
class Config extends base_command_1.default {
|
|
30
43
|
async run() {
|
|
31
44
|
const { args, argv } = await this.parse(Config);
|
|
@@ -46,6 +59,11 @@ class Config extends base_command_1.default {
|
|
|
46
59
|
if (!key) {
|
|
47
60
|
this.error("Usage: apso config get <key>");
|
|
48
61
|
}
|
|
62
|
+
// Friendly read-alias: `telemetry` reports on/off from telemetryDisabled.
|
|
63
|
+
if (key === "telemetry") {
|
|
64
|
+
this.log(config_1.globalConfig.read().telemetryDisabled ? "off" : "on");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
49
67
|
if (!VALID_KEYS.includes(key)) {
|
|
50
68
|
this.error(`Unknown key: ${key}\nValid keys: ${VALID_KEYS.join(", ")}`);
|
|
51
69
|
}
|
|
@@ -59,6 +77,16 @@ class Config extends base_command_1.default {
|
|
|
59
77
|
if (!key || value === undefined) {
|
|
60
78
|
this.error("Usage: apso config set <key> <value>");
|
|
61
79
|
}
|
|
80
|
+
// Friendly write-alias: `telemetry on|off` maps to telemetryDisabled.
|
|
81
|
+
if (key === "telemetry") {
|
|
82
|
+
const disabled = parseTelemetryToggle(value);
|
|
83
|
+
if (disabled === null) {
|
|
84
|
+
this.error(`Invalid telemetry value: ${value}. Use on or off.`);
|
|
85
|
+
}
|
|
86
|
+
config_1.globalConfig.set("telemetryDisabled", disabled);
|
|
87
|
+
this.log(`telemetry = ${disabled ? "off" : "on"}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
62
90
|
if (!VALID_KEYS.includes(key)) {
|
|
63
91
|
this.error(`Unknown key: ${key}\nValid keys: ${VALID_KEYS.join(", ")}`);
|
|
64
92
|
}
|
|
@@ -94,6 +122,7 @@ Config.examples = [
|
|
|
94
122
|
`$ apso config`,
|
|
95
123
|
`$ apso config get apiUrl`,
|
|
96
124
|
`$ apso config set verbose true`,
|
|
125
|
+
`$ apso config set telemetry off`,
|
|
97
126
|
`$ apso config reset`,
|
|
98
127
|
];
|
|
99
128
|
Config.strict = false;
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -13,8 +13,17 @@ export default class Dev extends BaseCommand {
|
|
|
13
13
|
/**
|
|
14
14
|
* TypeScript: run `npm run dev` which reads DATABASE_TYPE from .env.
|
|
15
15
|
* PGlite is handled natively by the NestJS app -- no sidecar needed.
|
|
16
|
+
*
|
|
17
|
+
* Prefer the `dev` script, but fall back to `start:dev` for templates that
|
|
18
|
+
* only define the latter (e.g. the v1 service template). See apsoai/cli#102.
|
|
16
19
|
*/
|
|
17
20
|
private runTypeScript;
|
|
21
|
+
/**
|
|
22
|
+
* Return the first script from `candidates` that exists in the local
|
|
23
|
+
* package.json. If package.json is missing or unparseable, fall back to the
|
|
24
|
+
* first candidate so behavior is unchanged when we cannot inspect it.
|
|
25
|
+
*/
|
|
26
|
+
private resolveNpmScript;
|
|
18
27
|
/**
|
|
19
28
|
* Python: if pglite, start the pglite-server sidecar first, then uvicorn.
|
|
20
29
|
*/
|
package/dist/commands/dev.js
CHANGED
|
@@ -52,10 +52,37 @@ class Dev extends base_command_1.default {
|
|
|
52
52
|
/**
|
|
53
53
|
* TypeScript: run `npm run dev` which reads DATABASE_TYPE from .env.
|
|
54
54
|
* PGlite is handled natively by the NestJS app -- no sidecar needed.
|
|
55
|
+
*
|
|
56
|
+
* Prefer the `dev` script, but fall back to `start:dev` for templates that
|
|
57
|
+
* only define the latter (e.g. the v1 service template). See apsoai/cli#102.
|
|
55
58
|
*/
|
|
56
59
|
async runTypeScript() {
|
|
57
|
-
this.
|
|
58
|
-
|
|
60
|
+
const script = this.resolveNpmScript(["dev", "start:dev"]);
|
|
61
|
+
if (!script) {
|
|
62
|
+
this.error('No "dev" or "start:dev" script found in package.json.\n' +
|
|
63
|
+
"Make sure you are in an Apso TypeScript project root.");
|
|
64
|
+
}
|
|
65
|
+
this.log(`Starting TypeScript dev server (npm run ${script})...`);
|
|
66
|
+
await this.spawnAndWait("npm", ["run", script]);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Return the first script from `candidates` that exists in the local
|
|
70
|
+
* package.json. If package.json is missing or unparseable, fall back to the
|
|
71
|
+
* first candidate so behavior is unchanged when we cannot inspect it.
|
|
72
|
+
*/
|
|
73
|
+
resolveNpmScript(candidates) {
|
|
74
|
+
const pkgPath = path.resolve("package.json");
|
|
75
|
+
if (!fs.existsSync(pkgPath))
|
|
76
|
+
return candidates[0];
|
|
77
|
+
try {
|
|
78
|
+
// eslint-disable-next-line unicorn/prefer-json-parse-buffer -- JSON.parse's TS type requires a string, not a Buffer
|
|
79
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
80
|
+
const scripts = (pkg.scripts || {});
|
|
81
|
+
return candidates.find((c) => typeof scripts[c] === "string");
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return candidates[0];
|
|
85
|
+
}
|
|
59
86
|
}
|
|
60
87
|
/**
|
|
61
88
|
* Python: if pglite, start the pglite-server sidecar first, then uvicorn.
|
|
@@ -96,19 +96,31 @@ class Generate extends base_command_1.default {
|
|
|
96
96
|
console.log(`[apso] Building... ${entity.name}`);
|
|
97
97
|
const entityBuildStart = perf_hooks_1.performance.now();
|
|
98
98
|
const entityRelationships = relationshipMap[entity.name] || [];
|
|
99
|
-
// Effective HTTP
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
99
|
+
// Effective HTTP flag: entity overrides the top-level default, which
|
|
100
|
+
// defaults to true. GraphQL uses resolvers, so the flag doesn't apply
|
|
101
|
+
// there. The flag only does real suppression where a generated route
|
|
102
|
+
// can't be cleanly overridden without removing it (see apsoai/cli#95):
|
|
103
|
+
// - TypeScript (NestJS): suppress the controller + its module wiring.
|
|
104
|
+
// - Go (Gin): suppress the handler + its route registration (Gin
|
|
105
|
+
// panics on a duplicate method+path, so overriding needs removal).
|
|
106
|
+
// - Python (FastAPI): NO-OP. FastAPI matches in registration order, so
|
|
107
|
+
// a hand-written APIRouter included before autogen_router in main.py
|
|
108
|
+
// shadows the generated route without removing it. We only emit an
|
|
109
|
+
// informational notice pointing at that pattern.
|
|
110
|
+
const effectiveHttp = (_b = (_a = entity.http) !== null && _a !== void 0 ? _a : http) !== null && _b !== void 0 ? _b : true;
|
|
111
|
+
const httpSuppressible = (language === "typescript" || language === "go") &&
|
|
112
|
+
lowerCaseApiType === "rest";
|
|
113
|
+
const includeController = httpSuppressible ? effectiveHttp : true;
|
|
114
|
+
if (httpSuppressible && !includeController) {
|
|
115
|
+
const artifact = language === "go" ? "handler + routes" : "controller";
|
|
116
|
+
console.log(`[apso] ${entity.name}: http=false — skipping generated ${artifact}`);
|
|
117
|
+
}
|
|
118
|
+
else if (language === "python" &&
|
|
119
|
+
lowerCaseApiType === "rest" &&
|
|
120
|
+
effectiveHttp === false) {
|
|
121
|
+
console.log(`[apso] ${entity.name}: http=false is a no-op for FastAPI — to replace a ` +
|
|
122
|
+
`generated route, include your own APIRouter before autogen_router in main.py ` +
|
|
123
|
+
`(first match wins). See the custom-endpoints skill.`);
|
|
112
124
|
}
|
|
113
125
|
const generationTasks = [
|
|
114
126
|
generator.generateEntity({
|
|
@@ -177,7 +189,7 @@ class Generate extends base_command_1.default {
|
|
|
177
189
|
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
178
190
|
}
|
|
179
191
|
// Generate index module
|
|
180
|
-
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents });
|
|
192
|
+
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType, { emitEvents, http });
|
|
181
193
|
for (const file of indexFiles) {
|
|
182
194
|
const fullPath = path.join(autogenPath, file.path);
|
|
183
195
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -20,7 +20,9 @@ function openBrowser(url) {
|
|
|
20
20
|
(0, child_process_1.exec)(command, () => resolve());
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
const sleep = (ms) => new Promise((r) =>
|
|
23
|
+
const sleep = (ms) => new Promise((r) => {
|
|
24
|
+
setTimeout(r, ms);
|
|
25
|
+
});
|
|
24
26
|
/**
|
|
25
27
|
* `apso github connect` — walk the user through connecting their GitHub
|
|
26
28
|
* account so the CLI can push generated code to a real repository.
|
package/dist/commands/link.js
CHANGED
|
@@ -81,6 +81,7 @@ class Link extends base_command_1.default {
|
|
|
81
81
|
else if (flags.create) {
|
|
82
82
|
this.log(`Creating service "${flags.create}"...`);
|
|
83
83
|
service = await (0, upgrade_1.withUpgradeRetry)(() => services_1.servicesApi.create(workspace.id, { name: flags.create }));
|
|
84
|
+
// eslint-disable-next-line no-negated-condition -- clearer as a non-interactive guard in this else-if chain
|
|
84
85
|
}
|
|
85
86
|
else if (!(0, interactive_1.isInteractive)()) {
|
|
86
87
|
this.error((0, interactive_1.missingFlag)("Pass --service <slug> to link an existing service, or --create <name> to make a new one."));
|
package/dist/commands/update.js
CHANGED
|
@@ -40,7 +40,7 @@ class Update extends base_command_1.default {
|
|
|
40
40
|
}
|
|
41
41
|
// Pick the upgrade command based on how the CLI was installed.
|
|
42
42
|
const installRoot = `${this.config.root || ""} ${process.execPath}`;
|
|
43
|
-
const isBrew = /
|
|
43
|
+
const isBrew = /cellar|homebrew|\/opt\/homebrew/i.test(installRoot);
|
|
44
44
|
const cmd = isBrew
|
|
45
45
|
? "brew upgrade apsoai/tap/apso"
|
|
46
46
|
: "npm install -g @apso/cli@latest";
|
package/dist/commands/use.js
CHANGED
|
@@ -55,7 +55,8 @@ class Use extends base_command_1.default {
|
|
|
55
55
|
this.log(` Now using workspace: ${ws.name} (${ws.slug})`);
|
|
56
56
|
// Show what's in the workspace so the next step is obvious.
|
|
57
57
|
try {
|
|
58
|
-
const
|
|
58
|
+
const listed = await services_1.servicesApi.list(ws.id);
|
|
59
|
+
const services = listed.data;
|
|
59
60
|
this.log("");
|
|
60
61
|
if (services.length > 0) {
|
|
61
62
|
this.log(" Services:");
|
|
@@ -7,6 +7,12 @@ const telemetry_1 = require("../../lib/telemetry/telemetry");
|
|
|
7
7
|
*/
|
|
8
8
|
const hook = async function (opts) {
|
|
9
9
|
(0, telemetry_1.initTelemetry)();
|
|
10
|
+
// First-run transparency notice (issue #96). Printed to stderr so it never
|
|
11
|
+
// pollutes stdout / piped output, shown once per install.
|
|
12
|
+
if ((0, telemetry_1.shouldShowFirstRunNotice)()) {
|
|
13
|
+
process.stderr.write(`\n${telemetry_1.FIRST_RUN_NOTICE}\n\n`);
|
|
14
|
+
(0, telemetry_1.markFirstRunNoticeShown)();
|
|
15
|
+
}
|
|
10
16
|
if (opts.id) {
|
|
11
17
|
(0, telemetry_1.track)("cli_command_started", { command: opts.id });
|
|
12
18
|
}
|
package/dist/lib/api/services.js
CHANGED
|
@@ -72,9 +72,9 @@ function adaptPaginated(raw, mapItem) {
|
|
|
72
72
|
const items = Array.isArray(raw) ? raw : (_a = raw === null || raw === void 0 ? void 0 : raw.data) !== null && _a !== void 0 ? _a : [];
|
|
73
73
|
const total = (_b = raw === null || raw === void 0 ? void 0 : raw.total) !== null && _b !== void 0 ? _b : items.length;
|
|
74
74
|
const page = (_c = raw === null || raw === void 0 ? void 0 : raw.page) !== null && _c !== void 0 ? _c : 1;
|
|
75
|
-
const pageSize = items.length
|
|
75
|
+
const pageSize = items.length > 0 ? items.length : 1;
|
|
76
76
|
return {
|
|
77
|
-
data: items.map(mapItem),
|
|
77
|
+
data: items.map((item) => mapItem(item)),
|
|
78
78
|
meta: {
|
|
79
79
|
total,
|
|
80
80
|
page,
|
|
@@ -98,7 +98,7 @@ exports.authApi = {
|
|
|
98
98
|
* clears stored credentials). Best-effort, never throws.
|
|
99
99
|
*/
|
|
100
100
|
async logout() {
|
|
101
|
-
|
|
101
|
+
// no-op: logout clears local credentials elsewhere; best-effort, never throws.
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
104
|
/**
|
|
@@ -109,7 +109,7 @@ exports.workspacesApi = {
|
|
|
109
109
|
var _a;
|
|
110
110
|
const raw = await client_1.api.get("/api/workspaces");
|
|
111
111
|
const items = Array.isArray(raw) ? raw : (_a = raw === null || raw === void 0 ? void 0 : raw.data) !== null && _a !== void 0 ? _a : [];
|
|
112
|
-
return items.map(adaptWorkspace);
|
|
112
|
+
return items.map((workspace) => adaptWorkspace(workspace));
|
|
113
113
|
},
|
|
114
114
|
async getById(id) {
|
|
115
115
|
const raw = await client_1.api.get(`/api/workspaces/${id}`);
|
|
@@ -261,7 +261,9 @@ exports.buildApi = {
|
|
|
261
261
|
options.onProgress(status);
|
|
262
262
|
if (status.status === "success" || status.status === "failed")
|
|
263
263
|
return status;
|
|
264
|
-
await new Promise((resolve) =>
|
|
264
|
+
await new Promise((resolve) => {
|
|
265
|
+
setTimeout(resolve, pollInterval);
|
|
266
|
+
});
|
|
265
267
|
}
|
|
266
268
|
/* eslint-enable no-await-in-loop */
|
|
267
269
|
throw new Error("Build timed out");
|
|
@@ -123,6 +123,16 @@ export interface GlobalConfigFile {
|
|
|
123
123
|
* Disable telemetry
|
|
124
124
|
*/
|
|
125
125
|
telemetryDisabled: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Anonymous install id (random UUID) for opt-out CLI telemetry. Generated
|
|
128
|
+
* once on first run and persisted here. Not derived from any machine
|
|
129
|
+
* identifier; carries no PII.
|
|
130
|
+
*/
|
|
131
|
+
installId?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the first-run telemetry transparency notice has been shown.
|
|
134
|
+
*/
|
|
135
|
+
telemetryNoticeShown: boolean;
|
|
126
136
|
}
|
|
127
137
|
/**
|
|
128
138
|
* Project link stored in .apso/link.json
|
package/dist/lib/config/types.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
|
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
|
|
12
|
-
* - env
|
|
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
|
|
59
|
+
return Boolean(telemetryDisabled);
|
|
50
60
|
}
|
|
51
|
-
/** Stable, non-PII fallback id
|
|
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
|
-
|
|
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 ((
|
|
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: (
|
|
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:
|
|
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.
|
|
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
|
-
<%
|
|
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
|
-
<%
|
|
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
|
-
<%
|
|
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";
|
|
@@ -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
|
*/
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "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.
|
|
9
|
+
"version": "0.29.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@biomejs/biome": "^1.9.4",
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.29.0",
|
|
3
3
|
"commands": {
|
|
4
4
|
"config": {
|
|
5
5
|
"id": "config",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"$ apso config",
|
|
15
15
|
"$ apso config get apiUrl",
|
|
16
16
|
"$ apso config set verbose true",
|
|
17
|
+
"$ apso config set telemetry off",
|
|
17
18
|
"$ apso config reset"
|
|
18
19
|
],
|
|
19
20
|
"flags": {},
|