@apso/cli 0.19.0 → 0.21.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/dist/commands/deploy.d.ts +12 -0
- package/dist/commands/deploy.js +80 -3
- package/dist/commands/generate.js +27 -8
- package/dist/commands/github/connect.d.ts +19 -0
- package/dist/commands/github/connect.js +95 -0
- package/dist/commands/init.js +5 -3
- package/dist/commands/link.js +3 -2
- package/dist/hooks/init/telemetry.d.ts +7 -0
- package/dist/hooks/init/telemetry.js +14 -0
- package/dist/hooks/postrun/telemetry.d.ts +8 -0
- package/dist/hooks/postrun/telemetry.js +18 -0
- package/dist/lib/api/client.d.ts +9 -0
- package/dist/lib/api/client.js +60 -1
- package/dist/lib/api/services.d.ts +45 -90
- package/dist/lib/api/services.js +259 -126
- package/dist/lib/base-command.d.ts +8 -0
- package/dist/lib/base-command.js +21 -0
- package/dist/lib/biome/biome.json +21 -0
- package/dist/lib/config/types.js +4 -1
- package/dist/lib/git.d.ts +25 -0
- package/dist/lib/git.js +113 -0
- package/dist/lib/telemetry/telemetry.d.ts +21 -0
- package/dist/lib/telemetry/telemetry.js +162 -0
- package/dist/lib/templates/go/header.eta +4 -3
- package/dist/lib/templates/guards/guards.module.eta +18 -12
- package/dist/lib/templates/header.eta +5 -7
- package/dist/lib/templates/python/header.eta +4 -4
- package/dist/lib/upgrade.d.ts +19 -0
- package/dist/lib/upgrade.js +86 -0
- package/npm-shrinkwrap.json +589 -12
- package/oclif.manifest.json +56 -1
- package/package.json +9 -2
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
2
|
export default abstract class BaseCommand extends Command {
|
|
3
|
+
/**
|
|
4
|
+
* Report failures to Sentry + PostHog before oclif exits. The postrun hook
|
|
5
|
+
* does not fire on error, so the flush has to happen here.
|
|
6
|
+
*/
|
|
7
|
+
catch(err: Error & {
|
|
8
|
+
exitCode?: number;
|
|
9
|
+
code?: string;
|
|
10
|
+
}): Promise<any>;
|
|
3
11
|
runCommand(command: string, args: string[], silent?: boolean): Promise<void>;
|
|
4
12
|
runNpmCommand(args: string[], silent?: boolean): Promise<void>;
|
|
5
13
|
}
|
package/dist/lib/base-command.js
CHANGED
|
@@ -4,7 +4,28 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const os_1 = tslib_1.__importDefault(require("os"));
|
|
7
|
+
const telemetry_1 = require("./telemetry/telemetry");
|
|
7
8
|
class BaseCommand extends core_1.Command {
|
|
9
|
+
/**
|
|
10
|
+
* Report failures to Sentry + PostHog before oclif exits. The postrun hook
|
|
11
|
+
* does not fire on error, so the flush has to happen here.
|
|
12
|
+
*/
|
|
13
|
+
async catch(err) {
|
|
14
|
+
try {
|
|
15
|
+
(0, telemetry_1.captureException)(err, { command: this.id });
|
|
16
|
+
(0, telemetry_1.track)("cli_command_failed", {
|
|
17
|
+
command: this.id,
|
|
18
|
+
duration_ms: (0, telemetry_1.elapsedMs)(),
|
|
19
|
+
error: err === null || err === void 0 ? void 0 : err.message,
|
|
20
|
+
error_code: err === null || err === void 0 ? void 0 : err.code,
|
|
21
|
+
});
|
|
22
|
+
await (0, telemetry_1.shutdownTelemetry)();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// never mask the original error with a telemetry failure
|
|
26
|
+
}
|
|
27
|
+
return super.catch(err);
|
|
28
|
+
}
|
|
8
29
|
async runCommand(command, args, silent = false) {
|
|
9
30
|
return new Promise((resolve, reject) => {
|
|
10
31
|
const isWindows = os_1.default.platform() === "win32";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"formatter": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"indentStyle": "space",
|
|
6
|
+
"indentWidth": 2,
|
|
7
|
+
"lineWidth": 100
|
|
8
|
+
},
|
|
9
|
+
"javascript": {
|
|
10
|
+
"parser": {
|
|
11
|
+
"unsafeParameterDecoratorsEnabled": true
|
|
12
|
+
},
|
|
13
|
+
"formatter": {
|
|
14
|
+
"quoteStyle": "double",
|
|
15
|
+
"semicolons": "always"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"linter": {
|
|
19
|
+
"enabled": false
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/lib/config/types.js
CHANGED
|
@@ -11,7 +11,10 @@ exports.CONFIG_ENV_VARS = exports.DEFAULT_GLOBAL_CONFIG = void 0;
|
|
|
11
11
|
* Default values for global configuration
|
|
12
12
|
*/
|
|
13
13
|
exports.DEFAULT_GLOBAL_CONFIG = {
|
|
14
|
-
|
|
14
|
+
// The CLI talks to the BFF (the app's /api/* routes), not the internal
|
|
15
|
+
// backend on api.apso.dev. Override for staging with
|
|
16
|
+
// APSO_API_URL=https://app.staging.apso.cloud.
|
|
17
|
+
apiUrl: "https://app.apso.cloud",
|
|
15
18
|
webUrl: "https://app.apso.cloud",
|
|
16
19
|
verbose: false,
|
|
17
20
|
noColor: false,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local git helpers for the deploy flow.
|
|
3
|
+
*
|
|
4
|
+
* The CLI pushes generated code to GitHub with the user's own local git (their
|
|
5
|
+
* credentials / credential helper) — not through apso.ai. The platform only
|
|
6
|
+
* needs the repo *connected* to the service so the provisioner can clone it at
|
|
7
|
+
* deploy time.
|
|
8
|
+
*/
|
|
9
|
+
export declare function gitAvailable(): boolean;
|
|
10
|
+
export declare function isGitRepo(cwd: string): boolean;
|
|
11
|
+
export declare function initRepo(cwd: string): void;
|
|
12
|
+
export declare function getOriginUrl(cwd: string): string | null;
|
|
13
|
+
/** Parse `owner/repo` from an https or ssh GitHub remote URL. */
|
|
14
|
+
export declare function parseRepoFullName(url: string | null): string | null;
|
|
15
|
+
/** The repo the local project is already wired to, if any (owner/repo). */
|
|
16
|
+
export declare function localRepoFullName(cwd: string): string | null;
|
|
17
|
+
export declare function setOrigin(cwd: string, repoUrl: string): void;
|
|
18
|
+
export declare function hasUncommittedChanges(cwd: string): boolean;
|
|
19
|
+
/** Stage everything and commit, unless there is nothing to commit. */
|
|
20
|
+
export declare function commitAll(cwd: string, message: string): boolean;
|
|
21
|
+
/** Attribution trailer appended to every commit the CLI creates. */
|
|
22
|
+
export declare const APSO_COMMIT_TRAILER = "Generated with Apso (https://apso.ai)";
|
|
23
|
+
/** Push the current HEAD to origin/<branch>, setting upstream. */
|
|
24
|
+
export declare function pushHead(cwd: string, branch: string): void;
|
|
25
|
+
export declare function httpsUrlFor(repoFullName: string): string;
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Local git helpers for the deploy flow.
|
|
4
|
+
*
|
|
5
|
+
* The CLI pushes generated code to GitHub with the user's own local git (their
|
|
6
|
+
* credentials / credential helper) — not through apso.ai. The platform only
|
|
7
|
+
* needs the repo *connected* to the service so the provisioner can clone it at
|
|
8
|
+
* deploy time.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.httpsUrlFor = exports.pushHead = exports.APSO_COMMIT_TRAILER = exports.commitAll = exports.hasUncommittedChanges = exports.setOrigin = exports.localRepoFullName = exports.parseRepoFullName = exports.getOriginUrl = exports.initRepo = exports.isGitRepo = exports.gitAvailable = void 0;
|
|
12
|
+
const tslib_1 = require("tslib");
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
15
|
+
const path = tslib_1.__importStar(require("path"));
|
|
16
|
+
function run(cmd, cwd) {
|
|
17
|
+
return (0, child_process_1.execSync)(cmd, {
|
|
18
|
+
cwd,
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
+
})
|
|
22
|
+
.toString()
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
function gitAvailable() {
|
|
26
|
+
try {
|
|
27
|
+
(0, child_process_1.execSync)("git --version", { stdio: "ignore" });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.gitAvailable = gitAvailable;
|
|
35
|
+
function isGitRepo(cwd) {
|
|
36
|
+
return fs.existsSync(path.join(cwd, ".git"));
|
|
37
|
+
}
|
|
38
|
+
exports.isGitRepo = isGitRepo;
|
|
39
|
+
function initRepo(cwd) {
|
|
40
|
+
if (!isGitRepo(cwd)) {
|
|
41
|
+
run("git init", cwd);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.initRepo = initRepo;
|
|
45
|
+
function getOriginUrl(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
return run("git remote get-url origin", cwd);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.getOriginUrl = getOriginUrl;
|
|
54
|
+
/** Parse `owner/repo` from an https or ssh GitHub remote URL. */
|
|
55
|
+
function parseRepoFullName(url) {
|
|
56
|
+
if (!url)
|
|
57
|
+
return null;
|
|
58
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/i);
|
|
59
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
60
|
+
}
|
|
61
|
+
exports.parseRepoFullName = parseRepoFullName;
|
|
62
|
+
/** The repo the local project is already wired to, if any (owner/repo). */
|
|
63
|
+
function localRepoFullName(cwd) {
|
|
64
|
+
return parseRepoFullName(getOriginUrl(cwd));
|
|
65
|
+
}
|
|
66
|
+
exports.localRepoFullName = localRepoFullName;
|
|
67
|
+
function setOrigin(cwd, repoUrl) {
|
|
68
|
+
if (getOriginUrl(cwd)) {
|
|
69
|
+
run(`git remote set-url origin ${repoUrl}`, cwd);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
run(`git remote add origin ${repoUrl}`, cwd);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.setOrigin = setOrigin;
|
|
76
|
+
function hasUncommittedChanges(cwd) {
|
|
77
|
+
return run("git status --porcelain", cwd).length > 0;
|
|
78
|
+
}
|
|
79
|
+
exports.hasUncommittedChanges = hasUncommittedChanges;
|
|
80
|
+
/** Stage everything and commit, unless there is nothing to commit. */
|
|
81
|
+
function commitAll(cwd, message) {
|
|
82
|
+
run("git add -A", cwd);
|
|
83
|
+
if (!hasUncommittedChanges(cwd)) {
|
|
84
|
+
// Something may still be staged with no worktree diff; check the index.
|
|
85
|
+
const staged = (() => {
|
|
86
|
+
try {
|
|
87
|
+
return run("git diff --cached --name-only", cwd).length > 0;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
if (!staged)
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// Second -m adds a trailer paragraph attributing the code to Apso. Mirrors
|
|
97
|
+
// the "Co-Authored-By" convention: a machine-visible footprint on every
|
|
98
|
+
// commit the CLI makes.
|
|
99
|
+
run(`git commit -m ${JSON.stringify(message)} -m ${JSON.stringify(exports.APSO_COMMIT_TRAILER)}`, cwd);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
exports.commitAll = commitAll;
|
|
103
|
+
/** Attribution trailer appended to every commit the CLI creates. */
|
|
104
|
+
exports.APSO_COMMIT_TRAILER = "Generated with Apso (https://apso.ai)";
|
|
105
|
+
/** Push the current HEAD to origin/<branch>, setting upstream. */
|
|
106
|
+
function pushHead(cwd, branch) {
|
|
107
|
+
run(`git push -u origin HEAD:${branch}`, cwd);
|
|
108
|
+
}
|
|
109
|
+
exports.pushHead = pushHead;
|
|
110
|
+
function httpsUrlFor(repoFullName) {
|
|
111
|
+
return `https://github.com/${repoFullName}.git`;
|
|
112
|
+
}
|
|
113
|
+
exports.httpsUrlFor = httpsUrlFor;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare function isDisabled(telemetryDisabled: boolean): boolean;
|
|
2
|
+
/** Stable, non-PII fallback id for logged-out users (no username, no MAC). */
|
|
3
|
+
declare function anonMachineId(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Initialize telemetry. Call once at process start (init hook). Safe to call
|
|
6
|
+
* when disabled — it just no-ops everything downstream.
|
|
7
|
+
*/
|
|
8
|
+
export declare function initTelemetry(): void;
|
|
9
|
+
/** Emit a PostHog event. No-op when disabled. */
|
|
10
|
+
export declare function track(event: string, properties?: Record<string, unknown>): void;
|
|
11
|
+
/** Report an exception to Sentry. No-op when disabled. */
|
|
12
|
+
export declare function captureException(err: unknown, context?: Record<string, unknown>): void;
|
|
13
|
+
/** Milliseconds since the process (CLI invocation) started. */
|
|
14
|
+
export declare function elapsedMs(): number;
|
|
15
|
+
/** Flush + close both clients. Call before the process exits. */
|
|
16
|
+
export declare function shutdownTelemetry(): Promise<void>;
|
|
17
|
+
export declare const __testing: {
|
|
18
|
+
isDisabled: typeof isDisabled;
|
|
19
|
+
anonMachineId: typeof anonMachineId;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.__testing = exports.shutdownTelemetry = exports.elapsedMs = exports.captureException = exports.track = exports.initTelemetry = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
/**
|
|
6
|
+
* CLI telemetry — usage analytics (PostHog) + error reporting (Sentry).
|
|
7
|
+
*
|
|
8
|
+
* Wired to the same Apso systems the web app and build engine use so CLI usage
|
|
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
|
|
13
|
+
*
|
|
14
|
+
* Everything here is best-effort and MUST NOT throw or block a command. All
|
|
15
|
+
* public functions swallow their own errors. The PostHog project keys and the
|
|
16
|
+
* Sentry DSN are public client ingestion keys (the same class of token shipped
|
|
17
|
+
* to browsers), safe to bundle.
|
|
18
|
+
*/
|
|
19
|
+
const posthog_node_1 = require("posthog-node");
|
|
20
|
+
const Sentry = tslib_1.__importStar(require("@sentry/node"));
|
|
21
|
+
const crypto_1 = require("crypto");
|
|
22
|
+
const os_1 = tslib_1.__importDefault(require("os"));
|
|
23
|
+
const config_1 = require("../config");
|
|
24
|
+
// Public ingestion keys (same tokens the browser/build-engine ship, per env).
|
|
25
|
+
const POSTHOG_KEY_PROD = "phc_Es9UUMw8pOs7Bs21b5PhfC5Qdo94GpYVv8MfdrG12LC";
|
|
26
|
+
const POSTHOG_KEY_STAGING = "phc_bkc33v3wo5LRvgPF6IdjsgDEDOwgJMz95MR2YIlvELk";
|
|
27
|
+
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
28
|
+
// Dedicated apsoai/apso-cli Sentry project DSN (public client key).
|
|
29
|
+
const SENTRY_DSN_DEFAULT = "https://754ae10322af78af3edbb87b93383ebb@o4509208616435714.ingest.us.sentry.io/4511796852162560";
|
|
30
|
+
let cliVersion = "0.0.0";
|
|
31
|
+
try {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
33
|
+
cliVersion = require("../../../package.json").version || cliVersion;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
let posthog = null;
|
|
39
|
+
let enabled = false;
|
|
40
|
+
let sentryOn = false;
|
|
41
|
+
let distinctId = "cli-anonymous";
|
|
42
|
+
let environment = "production";
|
|
43
|
+
let startedAt = Date.now();
|
|
44
|
+
function isDisabled(telemetryDisabled) {
|
|
45
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true")
|
|
46
|
+
return true;
|
|
47
|
+
if (process.env.APSO_TELEMETRY_DISABLED === "1")
|
|
48
|
+
return true;
|
|
49
|
+
return !!telemetryDisabled;
|
|
50
|
+
}
|
|
51
|
+
/** Stable, non-PII fallback id for logged-out users (no username, no MAC). */
|
|
52
|
+
function anonMachineId() {
|
|
53
|
+
const seed = `${os_1.default.hostname()}|${os_1.default.platform()}|${os_1.default.arch()}`;
|
|
54
|
+
return "cli_" + (0, crypto_1.createHash)("sha256").update(seed).digest("hex").slice(0, 20);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Initialize telemetry. Call once at process start (init hook). Safe to call
|
|
58
|
+
* when disabled — it just no-ops everything downstream.
|
|
59
|
+
*/
|
|
60
|
+
function initTelemetry() {
|
|
61
|
+
var _a, _b, _c;
|
|
62
|
+
try {
|
|
63
|
+
startedAt = Date.now();
|
|
64
|
+
const cfg = config_1.globalConfig.read();
|
|
65
|
+
if (isDisabled(cfg.telemetryDisabled)) {
|
|
66
|
+
enabled = false;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
enabled = true;
|
|
70
|
+
environment = (cfg.apiUrl || "").includes("staging")
|
|
71
|
+
? "staging"
|
|
72
|
+
: "production";
|
|
73
|
+
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();
|
|
75
|
+
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) {
|
|
77
|
+
// Attach identity so CLI events attribute to the same person as the app.
|
|
78
|
+
posthog.identify({
|
|
79
|
+
distinctId,
|
|
80
|
+
properties: { email: creds.user.email, name: creds.user.name },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const dsn = process.env.APSO_CLI_SENTRY_DSN || SENTRY_DSN_DEFAULT;
|
|
84
|
+
if (dsn) {
|
|
85
|
+
Sentry.init({
|
|
86
|
+
dsn,
|
|
87
|
+
environment,
|
|
88
|
+
release: `apso-cli@${cliVersion}`,
|
|
89
|
+
tracesSampleRate: 0,
|
|
90
|
+
});
|
|
91
|
+
Sentry.setUser({ id: distinctId, email: (_c = creds === null || creds === void 0 ? void 0 : creds.user) === null || _c === void 0 ? void 0 : _c.email });
|
|
92
|
+
Sentry.setTag("cli_version", cliVersion);
|
|
93
|
+
sentryOn = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
enabled = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.initTelemetry = initTelemetry;
|
|
101
|
+
function commonProps() {
|
|
102
|
+
return {
|
|
103
|
+
cli_version: cliVersion,
|
|
104
|
+
environment,
|
|
105
|
+
os: os_1.default.platform(),
|
|
106
|
+
arch: os_1.default.arch(),
|
|
107
|
+
node_version: process.version,
|
|
108
|
+
authenticated: distinctId !== anonMachineId() && !distinctId.startsWith("cli_"),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Emit a PostHog event. No-op when disabled. */
|
|
112
|
+
function track(event, properties = {}) {
|
|
113
|
+
try {
|
|
114
|
+
if (!enabled || !posthog)
|
|
115
|
+
return;
|
|
116
|
+
posthog.capture({
|
|
117
|
+
distinctId,
|
|
118
|
+
event,
|
|
119
|
+
properties: { ...commonProps(), ...properties },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// never break a command over telemetry
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.track = track;
|
|
127
|
+
/** Report an exception to Sentry. No-op when disabled. */
|
|
128
|
+
function captureException(err, context = {}) {
|
|
129
|
+
try {
|
|
130
|
+
if (!enabled || !sentryOn)
|
|
131
|
+
return;
|
|
132
|
+
Sentry.captureException(err, { extra: context });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// ignore
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.captureException = captureException;
|
|
139
|
+
/** Milliseconds since the process (CLI invocation) started. */
|
|
140
|
+
function elapsedMs() {
|
|
141
|
+
return Date.now() - startedAt;
|
|
142
|
+
}
|
|
143
|
+
exports.elapsedMs = elapsedMs;
|
|
144
|
+
/** Flush + close both clients. Call before the process exits. */
|
|
145
|
+
async function shutdownTelemetry() {
|
|
146
|
+
try {
|
|
147
|
+
if (posthog)
|
|
148
|
+
await posthog.shutdown();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// ignore
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (sentryOn)
|
|
155
|
+
await Sentry.close(2000);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.shutdownTelemetry = shutdownTelemetry;
|
|
162
|
+
exports.__testing = { isDisabled, anonMachineId };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
// Code generated by
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Code generated by Apso CLI. DO NOT EDIT.
|
|
2
|
+
// Generated with Apso (https://apso.ai)
|
|
3
|
+
//
|
|
4
|
+
// This file is auto-generated; changes will be overwritten on the next scaffold.
|
|
@@ -37,9 +37,11 @@ import { <%= it.authConfig.accountUserEntity %> } from '../<%= it.authConfig.acc
|
|
|
37
37
|
*
|
|
38
38
|
* Usage:
|
|
39
39
|
* 1. Import GuardsModule in your AppModule
|
|
40
|
-
* 2. Guards are
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* 2. Guards are ENABLED GLOBALLY BY DEFAULT (secure by default) because this
|
|
41
|
+
* schema declares an auth model / scoped entities. Requests are authenticated
|
|
42
|
+
* and tenant-scoped unless a route opts out with @Public().
|
|
43
|
+
* 3. Services with no auth model generate no guards and rely on the API Gateway
|
|
44
|
+
* API key at the edge.
|
|
43
45
|
*
|
|
44
46
|
<% if (it.authConfig) { %>
|
|
45
47
|
* AuthGuard:
|
|
@@ -88,18 +90,22 @@ import { <%= it.authConfig.accountUserEntity %> } from '../<%= it.authConfig.acc
|
|
|
88
90
|
<% if (it.scopedEntities && it.scopedEntities.length > 0) { %>
|
|
89
91
|
ScopeGuard,
|
|
90
92
|
<% } %>
|
|
91
|
-
//
|
|
93
|
+
// Global guards are ENABLED BY DEFAULT (secure by default) whenever the
|
|
94
|
+
// schema declares an auth model / scoped entities. Services with NO auth
|
|
95
|
+
// config generate no guards at all and are protected at the edge by the
|
|
96
|
+
// API Gateway API key — so this does not conflict with that setup. Use the
|
|
97
|
+
// @Public() decorator to opt an individual route out of authentication.
|
|
92
98
|
<% if (it.authConfig) { %>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
{
|
|
100
|
+
provide: APP_GUARD,
|
|
101
|
+
useClass: AuthGuard,
|
|
102
|
+
},
|
|
97
103
|
<% } %>
|
|
98
104
|
<% if (it.scopedEntities && it.scopedEntities.length > 0) { %>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
{
|
|
106
|
+
provide: APP_GUARD,
|
|
107
|
+
useClass: ScopeGuard,
|
|
108
|
+
},
|
|
103
109
|
<% } %>
|
|
104
110
|
],
|
|
105
111
|
exports: [
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* Generated with Apso (https://apso.ai)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
* Do not edit this file directly.
|
|
8
|
-
*/ %>
|
|
4
|
+
* This file is auto-generated by the Apso CLI. Do not edit it directly;
|
|
5
|
+
* changes will be overwritten on the next scaffold.
|
|
6
|
+
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
# Generated
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
#
|
|
2
|
+
# Generated with Apso (https://apso.ai)
|
|
3
|
+
#
|
|
4
|
+
# This file is auto-generated by the Apso CLI. Do not edit it directly;
|
|
5
|
+
# changes will be overwritten on the next scaffold.
|
|
6
6
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade-on-limit flow.
|
|
3
|
+
*
|
|
4
|
+
* When the platform rejects an action because the workspace hit a plan limit
|
|
5
|
+
* (e.g. the free-tier service cap), we surface a friendly prompt that opens the
|
|
6
|
+
* upgrade page in the browser and retries once the user is done — mirroring the
|
|
7
|
+
* "you've hit a limit, upgrade?" flow developers expect from modern CLIs.
|
|
8
|
+
*/
|
|
9
|
+
import { ApiClientError } from "./api/client";
|
|
10
|
+
/**
|
|
11
|
+
* Prompt the user to upgrade after a limit error, open the upgrade page, and
|
|
12
|
+
* wait for them to finish. Returns true if the caller should retry the action.
|
|
13
|
+
*/
|
|
14
|
+
export declare function promptUpgrade(error: ApiClientError): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Run an action; if it fails with an upgradeable plan-limit error, offer to
|
|
17
|
+
* upgrade and retry once.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withUpgradeRetry<T>(action: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Upgrade-on-limit flow.
|
|
4
|
+
*
|
|
5
|
+
* When the platform rejects an action because the workspace hit a plan limit
|
|
6
|
+
* (e.g. the free-tier service cap), we surface a friendly prompt that opens the
|
|
7
|
+
* upgrade page in the browser and retries once the user is done — mirroring the
|
|
8
|
+
* "you've hit a limit, upgrade?" flow developers expect from modern CLIs.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.withUpgradeRetry = exports.promptUpgrade = void 0;
|
|
12
|
+
const tslib_1 = require("tslib");
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
|
|
15
|
+
const client_1 = require("./api/client");
|
|
16
|
+
const config_1 = require("./config");
|
|
17
|
+
/**
|
|
18
|
+
* Open a URL in the default browser (best-effort).
|
|
19
|
+
*/
|
|
20
|
+
function openBrowser(url) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const platform = process.platform;
|
|
23
|
+
const command = platform === "darwin"
|
|
24
|
+
? `open "${url}"`
|
|
25
|
+
: platform === "win32"
|
|
26
|
+
? `start "" "${url}"`
|
|
27
|
+
: `xdg-open "${url}"`;
|
|
28
|
+
(0, child_process_1.exec)(command, () => resolve());
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Prompt the user to upgrade after a limit error, open the upgrade page, and
|
|
33
|
+
* wait for them to finish. Returns true if the caller should retry the action.
|
|
34
|
+
*/
|
|
35
|
+
async function promptUpgrade(error) {
|
|
36
|
+
const config = config_1.globalConfig.read();
|
|
37
|
+
const upgradeUrl = `${config.webUrl}${error.upgradeUrl || "/billing/upgrade"}`;
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(` ⚠ ${error.message || "You've reached a plan limit."}`);
|
|
40
|
+
console.log("");
|
|
41
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
42
|
+
{
|
|
43
|
+
type: "confirm",
|
|
44
|
+
name: "proceed",
|
|
45
|
+
message: "Open the upgrade page in your browser?",
|
|
46
|
+
default: true,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
if (!proceed) {
|
|
50
|
+
console.log(` You can upgrade any time at: ${upgradeUrl}`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(` Opening ${upgradeUrl}`);
|
|
55
|
+
console.log(" If the browser doesn't open, visit the URL above.");
|
|
56
|
+
await openBrowser(upgradeUrl);
|
|
57
|
+
const { done } = await inquirer_1.default.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: "confirm",
|
|
60
|
+
name: "done",
|
|
61
|
+
message: "Press enter once your upgrade is complete to retry",
|
|
62
|
+
default: true,
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
return done;
|
|
66
|
+
}
|
|
67
|
+
exports.promptUpgrade = promptUpgrade;
|
|
68
|
+
/**
|
|
69
|
+
* Run an action; if it fails with an upgradeable plan-limit error, offer to
|
|
70
|
+
* upgrade and retry once.
|
|
71
|
+
*/
|
|
72
|
+
async function withUpgradeRetry(action) {
|
|
73
|
+
try {
|
|
74
|
+
return await action();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof client_1.ApiClientError && error.isLimitError()) {
|
|
78
|
+
const retry = await promptUpgrade(error);
|
|
79
|
+
if (retry) {
|
|
80
|
+
return action();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.withUpgradeRetry = withUpgradeRetry;
|