@apso/cli 0.28.0 → 0.29.1
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/deploy/server-push.d.ts +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 +4 -1
- 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 +266 -425
- package/oclif.manifest.json +2 -1
- package/package.json +5 -5
|
@@ -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
|
*/
|