@seliseblocks/cli-os 0.2.11 → 0.2.12
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/AI_USAGE_GUIDE.md +551 -560
- package/README.md +171 -173
- package/dist/commands/auth/refresh.js +21 -2
- package/dist/commands/mfa/generate.js +7 -4
- package/dist/commands/mfa/method-set.js +13 -4
- package/dist/commands/mfa/totp-enable.d.ts +3 -3
- package/dist/commands/mfa/totp-enable.js +4 -4
- package/dist/commands/mfa/verify.js +4 -2
- package/dist/commands/projects/create.js +110 -16
- package/dist/index.js +696 -703
- package/package.json +2 -2
- package/dist/commands/sdk/client.d.ts +0 -1
- package/dist/commands/sdk/client.js +0 -99
- package/dist/commands/skill/add.d.ts +0 -1
- package/dist/commands/skill/add.js +0 -19
- package/dist/commands/skill/list.d.ts +0 -1
- package/dist/commands/skill/list.js +0 -15
- package/dist/commands/skill/show.d.ts +0 -1
- package/dist/commands/skill/show.js +0 -15
- package/dist/lib/skills.d.ts +0 -17
- package/dist/lib/skills.js +0 -69
- package/dist/skills/blocks-data-gateway-configuration/SKILL.md +0 -204
- package/dist/skills/blocks-data-gateway-crud/SKILL.md +0 -223
- package/dist/skills/blocks-data-storage/SKILL.md +0 -253
- package/dist/skills/blocks-data-storage/flows/object-management.md +0 -124
- package/dist/skills/blocks-frontend-local-https/SKILL.md +0 -100
- package/dist/skills/blocks-iam-access-control/SKILL.md +0 -49
- package/dist/skills/blocks-iam-access-control/flows/feature-gating.md +0 -38
- package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +0 -110
- package/dist/skills/blocks-iam-account/SKILL.md +0 -169
- package/dist/skills/blocks-iam-mfa/SKILL.md +0 -124
- package/dist/skills/blocks-iam-organizations/SKILL.md +0 -43
- package/dist/skills/blocks-iam-organizations/flows/admin-mutations.md +0 -89
- package/dist/skills/blocks-iam-organizations/flows/read-and-switch.md +0 -57
- package/dist/skills/blocks-iam-sso-oidc-configuration/SKILL.md +0 -105
- package/dist/skills/blocks-iam-sso-oidc-implementation/SKILL.md +0 -80
- package/dist/skills/blocks-iam-users/SKILL.md +0 -131
- package/dist/skills/blocks-localization-configuration/SKILL.md +0 -149
- package/dist/skills/blocks-localization-implementation/SKILL.md +0 -63
- package/dist/skills/blocks-mail/SKILL.md +0 -95
- package/dist/skills/blocks-notification/SKILL.md +0 -69
- package/dist/skills/blocks-notifier/SKILL.md +0 -107
- package/dist/skills/blocks-onboarding/SKILL.md +0 -77
- package/dist/skills/blocks-release-deployment/SKILL.md +0 -81
- package/dist/skills/blocks-secrets/SKILL.md +0 -81
- package/dist/skills/blocks-storage-configuration/SKILL.md +0 -93
- package/dist/skills/lint.mjs +0 -168
|
@@ -1,42 +1,136 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { booleanFlag, stringFlag } from "../../lib/args.js";
|
|
2
3
|
import { blocksRequest } from "../../lib/api.js";
|
|
3
4
|
import { confirmMutation } from "../../lib/confirm.js";
|
|
5
|
+
import { CliActionableError } from "../../lib/errors.js";
|
|
6
|
+
import { isRecord } from "../../lib/data-response.js";
|
|
4
7
|
import { writeOutput } from "../../lib/output.js";
|
|
5
|
-
import {
|
|
8
|
+
import { listProjectGroups } from "../../lib/project-info.js";
|
|
6
9
|
import { requestContext } from "../../lib/request-context.js";
|
|
10
|
+
import { parseCommand } from "../../lib/workspace.js";
|
|
11
|
+
const CREATE_ENDPOINT = "/os/v4/Project/Create";
|
|
12
|
+
// This command creates exactly one application, always in 'dev'. More
|
|
13
|
+
// environments are added later from the portal (or by a future command that
|
|
14
|
+
// passes tenantGroupId); nothing here may widen that.
|
|
15
|
+
const ENVIRONMENT = "dev";
|
|
16
|
+
// Project/Gets builds each tenantId as the environment letter prefixed onto
|
|
17
|
+
// the tenant group id -- 'd' for dev, uppercased.
|
|
18
|
+
const ENVIRONMENT_LETTER = "d";
|
|
19
|
+
const ENVIRONMENT_TENANT_PREFIX = ENVIRONMENT_LETTER.toUpperCase();
|
|
20
|
+
// Only ever a placeholder: the server validates that the domain parses as an
|
|
21
|
+
// absolute http(s) URL and then discards it, assigning its own domain during
|
|
22
|
+
// provisioning (environment letter + tenant slug, e.g.
|
|
23
|
+
// https://dboafe.slsblx.com). Mirror that shape so a dry-run payload looks
|
|
24
|
+
// like what the platform actually hands back.
|
|
25
|
+
const PLACEHOLDER_BASE_DOMAIN = "slsblx.com";
|
|
26
|
+
const PLACEHOLDER_COOKIE_DOMAIN = "slsblx.com";
|
|
27
|
+
const NAME_MIN_LENGTH = 3;
|
|
28
|
+
const NAME_MAX_LENGTH = 100;
|
|
29
|
+
const VERIFY_ATTEMPTS = 5;
|
|
30
|
+
const VERIFY_INTERVAL_MS = 2000;
|
|
7
31
|
export async function createProject(argv) {
|
|
8
32
|
const { args, flags } = parseCommand(argv);
|
|
9
|
-
const name = args[0] || stringFlag(flags, "name"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
33
|
+
const name = (args[0] || stringFlag(flags, "name")).trim();
|
|
34
|
+
if (!name) {
|
|
35
|
+
throw new CliActionableError("Missing project name.", "missing_project_name", 'blocks projects create "<name>"');
|
|
36
|
+
}
|
|
37
|
+
if (name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
|
|
38
|
+
throw new CliActionableError(`Project name must be between ${NAME_MIN_LENGTH} and ${NAME_MAX_LENGTH} characters.`, "invalid_project_name", `blocks projects create "<${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} character name>"`);
|
|
39
|
+
}
|
|
14
40
|
const body = {
|
|
15
41
|
name,
|
|
16
42
|
isAcceptBlocksTerms: true,
|
|
17
43
|
isUseBlocksExclusively: true,
|
|
18
|
-
isProduction:
|
|
44
|
+
isProduction: false,
|
|
19
45
|
resources: [],
|
|
20
46
|
applicationContexts: [
|
|
21
47
|
{
|
|
22
|
-
cookieDomain,
|
|
23
|
-
domain,
|
|
24
|
-
environment:
|
|
48
|
+
cookieDomain: PLACEHOLDER_COOKIE_DOMAIN,
|
|
49
|
+
domain: placeholderDomain(),
|
|
50
|
+
environment: ENVIRONMENT
|
|
25
51
|
}
|
|
26
52
|
]
|
|
27
53
|
};
|
|
28
54
|
if (booleanFlag(flags, "dry-run")) {
|
|
29
|
-
writeOutput({ dryRun: true, request: body }, flags);
|
|
55
|
+
writeOutput({ dryRun: true, endpoint: CREATE_ENDPOINT, request: body }, flags);
|
|
30
56
|
return;
|
|
31
57
|
}
|
|
32
|
-
|
|
33
|
-
|
|
58
|
+
if (!booleanFlag(flags, "allow-duplicate-name")) {
|
|
59
|
+
await assertNameIsFree(name, flags);
|
|
60
|
+
}
|
|
61
|
+
await confirmMutation(flags, `Create Blocks project '${name}' with a single '${ENVIRONMENT}' environment. This accepts the Blocks terms (isAcceptBlocksTerms, isUseBlocksExclusively) on your behalf.`);
|
|
62
|
+
// No tenantGroupId in the body: omitting it is what makes this a new
|
|
63
|
+
// project. Sending one would instead add an environment to an existing
|
|
64
|
+
// project, which this command deliberately cannot do.
|
|
65
|
+
const result = await blocksRequest(CREATE_ENDPOINT, {
|
|
34
66
|
accountAuth: true,
|
|
35
67
|
...requestContext(flags),
|
|
36
68
|
body
|
|
37
69
|
});
|
|
38
|
-
|
|
70
|
+
// Project/Create answers validation failures with HTTP 200 and
|
|
71
|
+
// { isSuccess: false, errors: { property: message } }, so the status code
|
|
72
|
+
// alone never proves the project was created.
|
|
73
|
+
if (!result?.isSuccess || !result.tenantGroupId) {
|
|
74
|
+
throw new CliActionableError(`Project/Create rejected '${name}': ${formatErrors(result?.errors)}`, "project_create_failed", "blocks projects list --json");
|
|
75
|
+
}
|
|
76
|
+
const tenantGroupId = result.tenantGroupId;
|
|
77
|
+
const created = await findCreatedProject(tenantGroupId, flags);
|
|
78
|
+
const tenantId = created?.tenantId ?? `${ENVIRONMENT_TENANT_PREFIX}${tenantGroupId}`;
|
|
79
|
+
writeOutput({
|
|
80
|
+
domain: created?.applications?.[0]?.domain ?? "",
|
|
81
|
+
environment: created?.environment ?? ENVIRONMENT,
|
|
82
|
+
name: created?.name ?? name,
|
|
83
|
+
tenantGroupId,
|
|
84
|
+
tenantId,
|
|
85
|
+
verified: Boolean(created)
|
|
86
|
+
}, flags);
|
|
87
|
+
if (!flags.json) {
|
|
88
|
+
if (!created) {
|
|
89
|
+
console.log(`Project/Gets has not listed '${tenantId}' yet. Re-run 'blocks projects list' to confirm before using it.`);
|
|
90
|
+
}
|
|
91
|
+
console.log(`Next: blocks use ${tenantId}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function assertNameIsFree(name, flags) {
|
|
95
|
+
const groups = await listProjectGroups(flags);
|
|
96
|
+
const target = name.toLowerCase();
|
|
97
|
+
const clash = groups.some((group) => (group.name ?? "").trim().toLowerCase() === target ||
|
|
98
|
+
(group.projects ?? []).some((project) => (project.name ?? "").trim().toLowerCase() === target));
|
|
99
|
+
if (!clash)
|
|
100
|
+
return;
|
|
101
|
+
// A retried create makes a second tenant group with the same name rather
|
|
102
|
+
// than failing, so an accidental duplicate is only preventable here.
|
|
103
|
+
throw new CliActionableError(`A project named '${name}' already exists on this account.`, "project_name_taken", "blocks projects list --json, or re-run with --allow-duplicate-name to create a second project with this name");
|
|
104
|
+
}
|
|
105
|
+
// Provisioning is asynchronous (Project/Create hands the tenant to a
|
|
106
|
+
// consumer queue) but fast, so poll Project/Gets briefly instead of
|
|
107
|
+
// reporting a tenantId the platform has not published yet.
|
|
108
|
+
async function findCreatedProject(tenantGroupId, flags) {
|
|
109
|
+
const expectedTenantId = `${ENVIRONMENT_TENANT_PREFIX}${tenantGroupId}`;
|
|
110
|
+
for (let attempt = 0; attempt < VERIFY_ATTEMPTS; attempt += 1) {
|
|
111
|
+
if (attempt > 0)
|
|
112
|
+
await delay(VERIFY_INTERVAL_MS);
|
|
113
|
+
const groups = await listProjectGroups(flags).catch(() => []);
|
|
114
|
+
const group = groups.find((item) => item.tenantGroupId === tenantGroupId);
|
|
115
|
+
const project = (group?.projects ?? []).find((item) => item.tenantId === expectedTenantId || item.environment === ENVIRONMENT);
|
|
116
|
+
if (project)
|
|
117
|
+
return project;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
function placeholderDomain() {
|
|
122
|
+
return `https://${ENVIRONMENT_LETTER}${randomSlug(5)}.${PLACEHOLDER_BASE_DOMAIN}`;
|
|
123
|
+
}
|
|
124
|
+
function randomSlug(length) {
|
|
125
|
+
const letters = "abcdefghijklmnopqrstuvwxyz";
|
|
126
|
+
return Array.from(randomBytes(length), (byte) => letters[byte % letters.length]).join("");
|
|
127
|
+
}
|
|
128
|
+
function formatErrors(errors) {
|
|
129
|
+
if (!isRecord(errors))
|
|
130
|
+
return "no error detail returned.";
|
|
131
|
+
const entries = Object.entries(errors).map(([key, value]) => `${key}: ${String(value)}`);
|
|
132
|
+
return entries.length > 0 ? entries.join("; ") : "no error detail returned.";
|
|
39
133
|
}
|
|
40
|
-
function
|
|
41
|
-
return
|
|
134
|
+
function delay(ms) {
|
|
135
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
136
|
}
|