@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.
Files changed (47) hide show
  1. package/AI_USAGE_GUIDE.md +551 -560
  2. package/README.md +171 -173
  3. package/dist/commands/auth/refresh.js +21 -2
  4. package/dist/commands/mfa/generate.js +7 -4
  5. package/dist/commands/mfa/method-set.js +13 -4
  6. package/dist/commands/mfa/totp-enable.d.ts +3 -3
  7. package/dist/commands/mfa/totp-enable.js +4 -4
  8. package/dist/commands/mfa/verify.js +4 -2
  9. package/dist/commands/projects/create.js +110 -16
  10. package/dist/index.js +696 -703
  11. package/package.json +2 -2
  12. package/dist/commands/sdk/client.d.ts +0 -1
  13. package/dist/commands/sdk/client.js +0 -99
  14. package/dist/commands/skill/add.d.ts +0 -1
  15. package/dist/commands/skill/add.js +0 -19
  16. package/dist/commands/skill/list.d.ts +0 -1
  17. package/dist/commands/skill/list.js +0 -15
  18. package/dist/commands/skill/show.d.ts +0 -1
  19. package/dist/commands/skill/show.js +0 -15
  20. package/dist/lib/skills.d.ts +0 -17
  21. package/dist/lib/skills.js +0 -69
  22. package/dist/skills/blocks-data-gateway-configuration/SKILL.md +0 -204
  23. package/dist/skills/blocks-data-gateway-crud/SKILL.md +0 -223
  24. package/dist/skills/blocks-data-storage/SKILL.md +0 -253
  25. package/dist/skills/blocks-data-storage/flows/object-management.md +0 -124
  26. package/dist/skills/blocks-frontend-local-https/SKILL.md +0 -100
  27. package/dist/skills/blocks-iam-access-control/SKILL.md +0 -49
  28. package/dist/skills/blocks-iam-access-control/flows/feature-gating.md +0 -38
  29. package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +0 -110
  30. package/dist/skills/blocks-iam-account/SKILL.md +0 -169
  31. package/dist/skills/blocks-iam-mfa/SKILL.md +0 -124
  32. package/dist/skills/blocks-iam-organizations/SKILL.md +0 -43
  33. package/dist/skills/blocks-iam-organizations/flows/admin-mutations.md +0 -89
  34. package/dist/skills/blocks-iam-organizations/flows/read-and-switch.md +0 -57
  35. package/dist/skills/blocks-iam-sso-oidc-configuration/SKILL.md +0 -105
  36. package/dist/skills/blocks-iam-sso-oidc-implementation/SKILL.md +0 -80
  37. package/dist/skills/blocks-iam-users/SKILL.md +0 -131
  38. package/dist/skills/blocks-localization-configuration/SKILL.md +0 -149
  39. package/dist/skills/blocks-localization-implementation/SKILL.md +0 -63
  40. package/dist/skills/blocks-mail/SKILL.md +0 -95
  41. package/dist/skills/blocks-notification/SKILL.md +0 -69
  42. package/dist/skills/blocks-notifier/SKILL.md +0 -107
  43. package/dist/skills/blocks-onboarding/SKILL.md +0 -77
  44. package/dist/skills/blocks-release-deployment/SKILL.md +0 -81
  45. package/dist/skills/blocks-secrets/SKILL.md +0 -81
  46. package/dist/skills/blocks-storage-configuration/SKILL.md +0 -93
  47. 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 { parseCommand } from "../../lib/workspace.js";
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", { required: true });
10
- const env = stringFlag(flags, "env", { defaultValue: "dev" });
11
- const domain = stringFlag(flags, "domain", { defaultValue: `https://${slug(name)}-${env}.seliseblocks.com` });
12
- const cookieDomain = stringFlag(flags, "cookie-domain", { defaultValue: "seliseblocks.com" });
13
- const production = booleanFlag(flags, "production");
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: production,
44
+ isProduction: false,
19
45
  resources: [],
20
46
  applicationContexts: [
21
47
  {
22
- cookieDomain,
23
- domain,
24
- environment: env
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
- await confirmMutation(flags, `Create Blocks project '${name}' with '${env}' environment.`);
33
- const result = await blocksRequest("/os/v4/Project/Create", {
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
- writeOutput(result, flags);
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 slug(value) {
41
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "blocks-app";
134
+ function delay(ms) {
135
+ return new Promise((resolve) => setTimeout(resolve, ms));
42
136
  }