@namostack/cli 0.1.0-rc.1 → 0.1.0-rc.2

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/index.mjs CHANGED
@@ -3690,10 +3690,37 @@ objectType({
3690
3690
  current: PasswordSchema,
3691
3691
  next: NewPasswordSchema
3692
3692
  });
3693
+ /** A one-time token from an emailed link (confirmation, password reset). */
3694
+ const EmailTokenSchema = stringType().min(1).max(256);
3695
+ objectType({ token: EmailTokenSchema });
3696
+ objectType({ email: EmailSchema });
3697
+ objectType({
3698
+ token: EmailTokenSchema,
3699
+ password: NewPasswordSchema
3700
+ });
3701
+ const DisplayNameSchema = stringType().trim().min(1).max(100);
3702
+ objectType({ displayName: DisplayNameSchema });
3703
+ objectType({ image: stringType().min(1).max(Math.ceil(524288 / 3) * 4).regex(/^[A-Za-z0-9+/]+={0,2}$/, "Image must be base64-encoded") });
3704
+ objectType({
3705
+ email: EmailSchema,
3706
+ password: PasswordSchema
3707
+ });
3708
+ objectType({ token: EmailTokenSchema });
3709
+ objectType({
3710
+ token: EmailTokenSchema,
3711
+ password: NewPasswordSchema
3712
+ });
3713
+ objectType({ password: PasswordSchema });
3693
3714
  const UserSchema = objectType({
3694
3715
  id: stringType(),
3695
3716
  email: stringType(),
3696
- displayName: stringType()
3717
+ displayName: stringType(),
3718
+ /** Confirmed by following the link in the signup email (or a password reset). Installing and publishing need it. */
3719
+ emailVerified: booleanType(),
3720
+ /** Path on the app's origin, e.g. `/avatars/usr_…/….jpg`. Unset until the user picks a photo. */
3721
+ avatarUrl: stringType().optional(),
3722
+ /** A new address waiting for its confirmation link; `email` stays the login until then. */
3723
+ pendingEmail: stringType().optional()
3697
3724
  });
3698
3725
  /** Signup and login: the session itself travels in the cookie. */
3699
3726
  const AuthResponseSchema = objectType({ user: UserSchema });
@@ -3706,106 +3733,36 @@ objectType({ sessions: arrayType(objectType({
3706
3733
  /** The session making this request. */
3707
3734
  current: booleanType()
3708
3735
  })) });
3736
+ objectType({
3737
+ /** `usr_<uuid>`, as auth-api generates them: nothing else reaches the handlers. */
3738
+ userId: stringType().regex(/^usr_[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/) });
3739
+ objectType({ developerName: DisplayNameSchema });
3709
3740
  objectType({ token: stringType().min(1) });
3741
+ /**
3742
+ * Whether the user has confirmed their email, as of this request. Missing counts as `false`, so a
3743
+ * token from before the claim existed (mid-deploy) can't pass the `requiresVerifiedEmail` routes.
3744
+ */
3745
+ const EmailVerifiedClaimSchema = booleanType().default(false);
3746
+ /** The user as of this request, for responses that show it (`GET /bootstrap`). Missing in a token from before the claims existed (mid-deploy). */
3747
+ const ProfileClaimsSchema = {
3748
+ email: stringType().optional(),
3749
+ displayName: stringType().optional(),
3750
+ avatarUrl: stringType().optional(),
3751
+ pendingEmail: stringType().optional()
3752
+ };
3710
3753
  discriminatedUnionType("kind", [objectType({
3711
3754
  kind: literalType("session"),
3712
3755
  userId: stringType(),
3713
- sessionId: stringType()
3756
+ sessionId: stringType(),
3757
+ emailVerified: EmailVerifiedClaimSchema,
3758
+ ...ProfileClaimsSchema
3714
3759
  }), objectType({
3715
3760
  kind: literalType("cliToken"),
3716
3761
  userId: stringType(),
3717
- tokenId: stringType()
3762
+ tokenId: stringType(),
3763
+ emailVerified: EmailVerifiedClaimSchema,
3764
+ ...ProfileClaimsSchema
3718
3765
  })]);
3719
- objectType({
3720
- code: enumType([
3721
- "permission_denied",
3722
- "not_found",
3723
- "validation_error",
3724
- "quota_exceeded",
3725
- "timeout",
3726
- "upgrade_required",
3727
- "internal"
3728
- ]).catch("internal"),
3729
- message: stringType()
3730
- });
3731
- objectType({ sdkVersion: stringType().min(1) });
3732
- /** Enough for a plugin using seedui: its `ThemeProvider` builds the rest from the mode. */
3733
- const PluginThemeSchema = objectType({ mode: enumType(["light", "dark"]) });
3734
- objectType({
3735
- /** In live testing (`env: 'dev'`), the slug: the plugin may not be published yet. */
3736
- pluginId: stringType(),
3737
- slug: stringType(),
3738
- /** `dev` while live testing with `namostack dev`: its data is kept apart from real data. */
3739
- env: enumType(["prod", "dev"]),
3740
- name: stringType(),
3741
- version: stringType(),
3742
- theme: PluginThemeSchema,
3743
- /** The browser's language, e.g. `en-US`. */
3744
- locale: stringType()
3745
- });
3746
- objectType({ displayName: stringType() });
3747
- objectType({
3748
- message: stringType().trim().min(1).max(300),
3749
- tone: enumType([
3750
- "info",
3751
- "success",
3752
- "warning",
3753
- "error"
3754
- ]).default("info")
3755
- });
3756
- numberType().int().min(0).max(1e5);
3757
- stringType().min(1);
3758
- //#endregion
3759
- //#region ../../packages/types/src/cli.ts
3760
- /** Shown in Settings → Developer; the CLI suggests its hostname, the user confirms it when approving. */
3761
- const TokenNameSchema = stringType().trim().min(1).max(100);
3762
- /** `WDJB-MJHT`: what the user checks in the browser against what the CLI printed. */
3763
- const UserCodeSchema = stringType().trim().toUpperCase().regex(/^[A-Z]{4}-[A-Z]{4}$/, "Invalid code");
3764
- objectType({ tokenName: TokenNameSchema });
3765
- const CliAuthStartResponseSchema = objectType({
3766
- /** Secret the CLI polls with; never shown to the user. */
3767
- deviceCode: stringType(),
3768
- userCode: stringType(),
3769
- /** The web app's approval page for this login. */
3770
- verifyUrl: stringType().url(),
3771
- /** Seconds to wait between polls. */
3772
- interval: numberType().int(),
3773
- /** Seconds before the login request expires. */
3774
- expiresIn: numberType().int()
3775
- });
3776
- objectType({ deviceCode: stringType().min(1) });
3777
- /** `approved` is answered once: the token is issued and the request consumed. */
3778
- const CliAuthPollResponseSchema = discriminatedUnionType("status", [
3779
- objectType({ status: literalType("pending") }),
3780
- objectType({
3781
- status: literalType("approved"),
3782
- /** `nst_pat_…`: the CLI stores it, the server only keeps its hash. */
3783
- token: stringType(),
3784
- expiresAt: TimestampSchema
3785
- }),
3786
- objectType({ status: literalType("denied") }),
3787
- objectType({ status: literalType("expired") })
3788
- ]);
3789
- objectType({ userCode: UserCodeSchema });
3790
- /** `GET /cli/auth/requests/:userCode`: what the approval page shows. */
3791
- const CliAuthRequestSchema = objectType({
3792
- userCode: stringType(),
3793
- tokenName: stringType()
3794
- });
3795
- objectType({
3796
- userCode: UserCodeSchema,
3797
- tokenName: TokenNameSchema
3798
- });
3799
- /** `GET /cli/tokens`: the user's active tokens, newest first. */
3800
- const CliTokensResponseSchema = objectType({ tokens: arrayType(objectType({
3801
- id: stringType(),
3802
- name: stringType(),
3803
- /** Absent until the CLI first uses the token. */
3804
- lastUsedAt: TimestampSchema.optional(),
3805
- expiresAt: TimestampSchema
3806
- })) });
3807
- objectType({ tokenId: stringType().min(1) });
3808
- objectType({ token: stringType().min(1) });
3809
3766
  //#endregion
3810
3767
  //#region ../../packages/types/src/plugins.ts
3811
3768
  /** `public`: in the catalog. `unlisted`: installable by direct link. `private`: owner only. */
@@ -3953,8 +3910,8 @@ const StartDevSessionResponseSchema = objectType({
3953
3910
  /** Seconds before the session ends unless the CLI sends it again. */
3954
3911
  expiresIn: numberType().int()
3955
3912
  });
3956
- /** `GET /dev-sessions`: the caller's live sessions. */
3957
- const DevSessionsResponseSchema = objectType({ devSessions: arrayType(objectType({
3913
+ /** A plugin in live testing, as its author's web app shows it. */
3914
+ const DevSessionSchema = objectType({
3958
3915
  slug: stringType(),
3959
3916
  name: stringType(),
3960
3917
  version: stringType(),
@@ -3963,24 +3920,139 @@ const DevSessionsResponseSchema = objectType({ devSessions: arrayType(objectType
3963
3920
  permissions: arrayType(PluginPermissionSchema),
3964
3921
  iconUrl: stringType().optional(),
3965
3922
  startedAt: TimestampSchema
3966
- })) });
3967
- const ApiErrorSchema = objectType({
3923
+ });
3924
+ /** `GET /dev-sessions`: the caller's live sessions. */
3925
+ const DevSessionsResponseSchema = objectType({ devSessions: arrayType(DevSessionSchema) });
3926
+ /** `GET /preferences` answers it, `PUT /preferences` replaces it whole. */
3927
+ const PreferencesSchema = objectType({
3928
+ theme: enumType([
3929
+ "light",
3930
+ "dark",
3931
+ "system"
3932
+ ]),
3933
+ sidebarCollapsed: booleanType(),
3934
+ /** Applies plugin updates on its own, except those adding permissions. */
3935
+ autoUpdatePlugins: booleanType()
3936
+ });
3937
+ //#endregion
3938
+ //#region ../../packages/types/src/bootstrap.ts
3939
+ /** `GET /bootstrap`: everything the web app needs to show its first page, in one request. */
3940
+ const BootstrapResponseSchema = objectType({
3941
+ user: UserSchema,
3942
+ preferences: PreferencesSchema,
3943
+ installedPlugins: arrayType(InstalledPluginSchema),
3944
+ devSessions: arrayType(DevSessionSchema)
3945
+ });
3946
+ objectType({
3968
3947
  code: enumType([
3969
- "unauthenticated",
3970
- "invalid_credentials",
3971
3948
  "permission_denied",
3972
3949
  "not_found",
3973
- "email_unavailable",
3974
- "already_installed",
3975
- "slug_taken",
3976
- "rate_limited",
3977
- "quota_exceeded",
3978
3950
  "validation_error",
3951
+ "quota_exceeded",
3952
+ "timeout",
3979
3953
  "upgrade_required",
3980
3954
  "internal"
3981
3955
  ]).catch("internal"),
3982
3956
  message: stringType()
3983
3957
  });
3958
+ objectType({ sdkVersion: stringType().min(1) });
3959
+ /** Enough for a plugin using seedui: its `ThemeProvider` builds the rest from the mode. */
3960
+ const PluginThemeSchema = objectType({ mode: enumType(["light", "dark"]) });
3961
+ objectType({
3962
+ /** In live testing (`env: 'dev'`), the slug: the plugin may not be published yet. */
3963
+ pluginId: stringType(),
3964
+ slug: stringType(),
3965
+ /** `dev` while live testing with `namostack dev`: its data is kept apart from real data. */
3966
+ env: enumType(["prod", "dev"]),
3967
+ name: stringType(),
3968
+ version: stringType(),
3969
+ theme: PluginThemeSchema,
3970
+ /** The browser's language, e.g. `en-US`. */
3971
+ locale: stringType()
3972
+ });
3973
+ objectType({ displayName: stringType() });
3974
+ objectType({
3975
+ message: stringType().trim().min(1).max(300),
3976
+ tone: enumType([
3977
+ "info",
3978
+ "success",
3979
+ "warning",
3980
+ "error"
3981
+ ]).default("info")
3982
+ });
3983
+ numberType().int().min(0).max(1e5);
3984
+ stringType().min(1);
3985
+ //#endregion
3986
+ //#region ../../packages/types/src/cli.ts
3987
+ /** Shown in Settings → Developer; the CLI suggests its hostname, the user confirms it when approving. */
3988
+ const TokenNameSchema = stringType().trim().min(1).max(100);
3989
+ /** `WDJB-MJHT`: what the user checks in the browser against what the CLI printed. */
3990
+ const UserCodeSchema = stringType().trim().toUpperCase().regex(/^[A-Z]{4}-[A-Z]{4}$/, "Invalid code");
3991
+ objectType({ tokenName: TokenNameSchema });
3992
+ const CliAuthStartResponseSchema = objectType({
3993
+ /** Secret the CLI polls with; never shown to the user. */
3994
+ deviceCode: stringType(),
3995
+ userCode: stringType(),
3996
+ /** The web app's approval page for this login. */
3997
+ verifyUrl: stringType().url(),
3998
+ /** Seconds to wait between polls. */
3999
+ interval: numberType().int(),
4000
+ /** Seconds before the login request expires. */
4001
+ expiresIn: numberType().int()
4002
+ });
4003
+ objectType({ deviceCode: stringType().min(1) });
4004
+ /** `approved` is answered once: the token is issued and the request consumed. */
4005
+ const CliAuthPollResponseSchema = discriminatedUnionType("status", [
4006
+ objectType({ status: literalType("pending") }),
4007
+ objectType({
4008
+ status: literalType("approved"),
4009
+ /** `nst_pat_…`: the CLI stores it, the server only keeps its hash. */
4010
+ token: stringType(),
4011
+ expiresAt: TimestampSchema
4012
+ }),
4013
+ objectType({ status: literalType("denied") }),
4014
+ objectType({ status: literalType("expired") })
4015
+ ]);
4016
+ objectType({ userCode: UserCodeSchema });
4017
+ /** `GET /cli/auth/requests/:userCode`: what the approval page shows. */
4018
+ const CliAuthRequestSchema = objectType({
4019
+ userCode: stringType(),
4020
+ tokenName: stringType()
4021
+ });
4022
+ objectType({
4023
+ userCode: UserCodeSchema,
4024
+ tokenName: TokenNameSchema
4025
+ });
4026
+ /** `GET /cli/tokens`: the user's active tokens, newest first. */
4027
+ const CliTokensResponseSchema = objectType({ tokens: arrayType(objectType({
4028
+ id: stringType(),
4029
+ name: stringType(),
4030
+ /** Absent until the CLI first uses the token. */
4031
+ lastUsedAt: TimestampSchema.optional(),
4032
+ expiresAt: TimestampSchema
4033
+ })) });
4034
+ objectType({ tokenId: stringType().min(1) });
4035
+ objectType({ token: stringType().min(1) });
4036
+ const ErrorCodeSchema = enumType([
4037
+ "unauthenticated",
4038
+ "invalid_credentials",
4039
+ "permission_denied",
4040
+ "not_found",
4041
+ "email_unavailable",
4042
+ "email_unverified",
4043
+ "already_installed",
4044
+ "published_plugins",
4045
+ "slug_taken",
4046
+ "rate_limited",
4047
+ "quota_exceeded",
4048
+ "validation_error",
4049
+ "upgrade_required",
4050
+ "internal"
4051
+ ]);
4052
+ const ApiErrorSchema = objectType({
4053
+ code: stringType().pipe(ErrorCodeSchema.catch("internal")),
4054
+ message: stringType()
4055
+ });
3984
4056
  /**
3985
4057
  * Base for every domain error. `code` decides the HTTP status (api-kit maps it),
3986
4058
  * and `message` is sent to the client as is, so it must be safe to show.
@@ -4046,20 +4118,9 @@ const FindResponseSchema = objectType({
4046
4118
  /** Present while there may be more matching documents. */
4047
4119
  cursor: stringType().optional()
4048
4120
  });
4049
- /** `GET /preferences` answers it, `PUT /preferences` replaces it whole. */
4050
- const PreferencesSchema = objectType({
4051
- theme: enumType([
4052
- "light",
4053
- "dark",
4054
- "system"
4055
- ]),
4056
- sidebarCollapsed: booleanType(),
4057
- /** Applies plugin updates on its own, except those adding permissions. */
4058
- autoUpdatePlugins: booleanType()
4059
- });
4060
4121
  //#endregion
4061
4122
  //#region ../../packages/libraries/plugin-sdk/package.json
4062
- var version$1 = "0.1.0-rc.1";
4123
+ var version$1 = "0.1.0-rc.2";
4063
4124
  //#endregion
4064
4125
  //#region src/paths.ts
4065
4126
  /** `pnpm dev:cli` runs the `.ts` sources; npm installs the bundle (`dist/index.mjs`). */
@@ -4315,8 +4376,75 @@ function createApiClient({ baseUrl, token, client }) {
4315
4376
  method: "GET",
4316
4377
  path: "/auth/me",
4317
4378
  schema: UserSchema
4379
+ }),
4380
+ verifyEmail: (body) => request({
4381
+ method: "POST",
4382
+ path: "/auth/email/verify",
4383
+ body
4384
+ }),
4385
+ resendVerification: () => request({
4386
+ method: "POST",
4387
+ path: "/auth/email/resend"
4388
+ }),
4389
+ forgotPassword: (body) => request({
4390
+ method: "POST",
4391
+ path: "/auth/password/forgot",
4392
+ body
4393
+ }),
4394
+ resetPassword: (body) => request({
4395
+ method: "POST",
4396
+ path: "/auth/password/reset",
4397
+ body
4398
+ }),
4399
+ updateProfile: (body) => request({
4400
+ method: "PATCH",
4401
+ path: "/auth/me",
4402
+ body,
4403
+ schema: UserSchema
4404
+ }),
4405
+ setAvatar: (body) => request({
4406
+ method: "PUT",
4407
+ path: "/auth/me/avatar",
4408
+ body,
4409
+ schema: UserSchema
4410
+ }),
4411
+ removeAvatar: () => request({
4412
+ method: "DELETE",
4413
+ path: "/auth/me/avatar",
4414
+ schema: UserSchema
4415
+ }),
4416
+ changeEmail: (body) => request({
4417
+ method: "POST",
4418
+ path: "/auth/email/change",
4419
+ body,
4420
+ schema: UserSchema
4421
+ }),
4422
+ cancelEmailChange: () => request({
4423
+ method: "DELETE",
4424
+ path: "/auth/email/change",
4425
+ schema: UserSchema
4426
+ }),
4427
+ confirmEmailChange: (body) => request({
4428
+ method: "POST",
4429
+ path: "/auth/email/change/confirm",
4430
+ body
4431
+ }),
4432
+ revertEmailChange: (body) => request({
4433
+ method: "POST",
4434
+ path: "/auth/email/revert",
4435
+ body
4436
+ }),
4437
+ deleteAccount: (body) => request({
4438
+ method: "POST",
4439
+ path: "/auth/me/delete",
4440
+ body
4318
4441
  })
4319
4442
  },
4443
+ bootstrap: { get: () => request({
4444
+ method: "GET",
4445
+ path: "/bootstrap",
4446
+ schema: BootstrapResponseSchema
4447
+ }) },
4320
4448
  preferences: {
4321
4449
  get: () => request({
4322
4450
  method: "GET",
@@ -4466,7 +4594,7 @@ function createApiClient({ baseUrl, token, client }) {
4466
4594
  //#endregion
4467
4595
  //#region src/version.ts
4468
4596
  /** Sent to the APIs on every call (`cli/<version>`) and printed by `--version`. */
4469
- const CLI_VERSION = "0.1.0-rc.1";
4597
+ const CLI_VERSION = "0.1.0-rc.2";
4470
4598
  //#endregion
4471
4599
  //#region src/api.ts
4472
4600
  const DEFAULT_API_URL = semver.prerelease(CLI_VERSION) ? CLI_CONFIG.apiUrls.staging : CLI_CONFIG.apiUrls.prod;
@@ -4529,9 +4657,36 @@ async function deleteCredentials(apiUrl) {
4529
4657
  }
4530
4658
  //#endregion
4531
4659
  //#region src/utils/json.ts
4532
- /** Two-space indented, with a final newline, like the files developers edit by hand. */
4660
+ /** Prettier's default; arrays that fit on one line stay on one line. */
4661
+ const PRINT_WIDTH = 80;
4662
+ const INDENT = " ";
4663
+ /** Written as Prettier would format it, so generated files pass `prettier --check`. */
4533
4664
  async function writeJson(path, value) {
4534
- await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
4665
+ const text = basename(path) === "package.json" ? JSON.stringify(value, null, 2) : format(value, "", 0, false);
4666
+ await writeFile(path, `${text}\n`);
4667
+ }
4668
+ /** `column` is where the value starts; `comma`, whether one follows it on the line. */
4669
+ function format(value, indent, column, comma) {
4670
+ if (Array.isArray(value)) {
4671
+ if (value.length === 0) return "[]";
4672
+ const inline = `[${value.map((item) => JSON.stringify(item)).join(", ")}]`;
4673
+ if (value.every(isPrimitive) && column + inline.length + (comma ? 1 : 0) <= PRINT_WIDTH) return inline;
4674
+ const inner = indent + INDENT;
4675
+ return `[\n${value.map((item, index) => inner + format(item, inner, inner.length, index < value.length - 1)).join(",\n")}\n${indent}]`;
4676
+ }
4677
+ if (value !== null && typeof value === "object") {
4678
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0);
4679
+ if (entries.length === 0) return "{}";
4680
+ const inner = indent + INDENT;
4681
+ return `{\n${entries.map(([key, item], index) => {
4682
+ const prefix = `${inner}${JSON.stringify(key)}: `;
4683
+ return prefix + format(item, inner, prefix.length, index < entries.length - 1);
4684
+ }).join(",\n")}\n${indent}}`;
4685
+ }
4686
+ return JSON.stringify(value);
4687
+ }
4688
+ function isPrimitive(value) {
4689
+ return value === null || typeof value !== "object";
4535
4690
  }
4536
4691
  //#endregion
4537
4692
  //#region src/utils/prompts.ts
@@ -4574,37 +4729,67 @@ const createCommand = defineCommand({
4574
4729
  dir: {
4575
4730
  type: "string",
4576
4731
  description: "Folder to create the plugin in (default: the current one)"
4732
+ },
4733
+ slug: {
4734
+ type: "string",
4735
+ description: "Slug (default: from the name)"
4736
+ },
4737
+ template: {
4738
+ type: "string",
4739
+ description: `${CLI_CONFIG.templates.join(" or ")} (default: ${CLI_CONFIG.templates[0]})`
4740
+ },
4741
+ permissions: {
4742
+ type: "string",
4743
+ description: "Comma-separated, \"\" for none (default: all)"
4744
+ },
4745
+ "package-manager": {
4746
+ type: "string",
4747
+ description: `${CLI_CONFIG.packageManagers.join(", ")} or none (default: the one running the CLI)`
4748
+ },
4749
+ yes: {
4750
+ type: "boolean",
4751
+ description: "No prompts: defaults for anything not passed"
4577
4752
  }
4578
4753
  },
4579
4754
  async run({ args }) {
4580
4755
  const apiUrl = resolveApiUrl(args["api-url"]);
4756
+ const parentDir = resolve(args.dir ?? ".");
4757
+ const inHouse = isInHouseFolder(parentDir);
4758
+ const templateArg = parseChoice("--template", args.template, CLI_CONFIG.templates);
4759
+ const permissionsArg = parsePermissions(args.permissions);
4760
+ const packageManagerArg = parseChoice("--package-manager", args["package-manager"], [...CLI_CONFIG.packageManagers, "none"]);
4761
+ if (args.yes && !args.name) fail("--yes needs the plugin name: namostack create <name> --yes");
4581
4762
  intro(pc.bold("namostack create"));
4582
- const name = answered(await text({
4763
+ const name = (args.yes ? checkName(args.name) : answered(await text({
4583
4764
  message: "Plugin name",
4584
4765
  initialValue: args.name,
4585
- validate: (value) => PluginManifestSchema.shape.name.safeParse(value).success ? void 0 : "Use 1–50 characters."
4586
- })).trim();
4587
- const parentDir = resolve(args.dir ?? ".");
4588
- const slug = await promptSlug(apiUrl, toSlug(name), parentDir);
4766
+ validate: (value) => nameProblem(value)
4767
+ }))).trim();
4768
+ const slug = await resolveSlug(apiUrl, parentDir, {
4769
+ suggestion: args.slug ?? toSlug(name),
4770
+ fromFlag: args.slug !== void 0,
4771
+ yes: Boolean(args.yes)
4772
+ });
4589
4773
  const dir = join(parentDir, slug);
4590
4774
  const displayDir = relative(process.cwd(), dir);
4591
- const inHouse = isInHouseFolder(parentDir);
4592
- const template = answered(await select({
4775
+ const template = templateArg ?? (args.yes ? CLI_CONFIG.templates[0] : answered(await select({
4593
4776
  message: "Template",
4594
4777
  options: CLI_CONFIG.templates.map((value) => ({
4595
4778
  value,
4596
4779
  label: TEMPLATE_LABELS[value]
4597
4780
  }))
4598
- }));
4599
- const permissions = answered(await multiselect({
4781
+ })));
4782
+ const permissions = permissionsArg ?? (args.yes ? [...PluginPermissionSchema.options] : answered(await multiselect({
4600
4783
  message: "Permissions (users see these before installing)",
4601
4784
  options: PluginPermissionSchema.options.map((value) => ({
4602
4785
  value,
4603
4786
  hint: PERMISSION_HINTS[value]
4604
4787
  })),
4788
+ initialValues: [...PluginPermissionSchema.options],
4605
4789
  required: false
4606
- }));
4607
- const packageManager = inHouse ? "pnpm" : answered(await select({
4790
+ })));
4791
+ if (inHouse && packageManagerArg && packageManagerArg !== "pnpm") log.warn("In-house plugins install with pnpm: ignored --package-manager.");
4792
+ const packageManager = inHouse ? "pnpm" : packageManagerArg ? packageManagerArg === "none" ? null : packageManagerArg : args.yes ? detectPackageManager() : answered(await select({
4608
4793
  message: "Install dependencies with",
4609
4794
  initialValue: detectPackageManager(),
4610
4795
  options: [...CLI_CONFIG.packageManagers.map((value) => ({ value })), {
@@ -4642,26 +4827,64 @@ const createCommand = defineCommand({
4642
4827
  }
4643
4828
  });
4644
4829
  /** Asks until the slug is valid and free on disk, then confirms it if the API flags it (docs/plugins.md). */
4645
- async function promptSlug(apiUrl, suggestion, parentDir) {
4646
- for (;;) {
4647
- const slug = answered(await text({
4830
+ async function resolveSlug(apiUrl, parentDir, { suggestion, fromFlag, yes }) {
4831
+ for (let first = true;; first = false) {
4832
+ const slug = first && (fromFlag || yes) ? checkSlugFormat(suggestion, parentDir) : answered(await text({
4648
4833
  message: "Slug (your plugin's unique name)",
4649
4834
  initialValue: suggestion,
4650
- validate: (value) => {
4651
- const parsed = PluginSlugSchema.safeParse(value);
4652
- if (!parsed.success) return parsed.error.issues[0]?.message;
4653
- return existsSync(join(parentDir, parsed.data)) ? `A folder named ${parsed.data} already exists in ${parentDir}.` : void 0;
4654
- }
4835
+ validate: (value) => slugProblem(value ?? "", parentDir)
4655
4836
  }));
4656
4837
  const availability = await checkSlug(apiUrl, slug);
4657
4838
  if (!availability || availability.available && availability.publishable) return slug;
4839
+ const message = !availability.available ? `You already have a plugin named ${slug}: publishing this one would publish a new version of it.` : `${slug} is used by another public or unlisted plugin: this one could only be published as private.`;
4840
+ if (yes) {
4841
+ if (!availability.available) fail(`${message} Pass another --slug.`);
4842
+ log.warn(message);
4843
+ return slug;
4844
+ }
4658
4845
  if (answered(await confirm({
4659
- message: !availability.available ? `You already have a plugin named ${slug}: publishing this one would publish a new version of it. Keep it anyway?` : `${slug} is used by another public or unlisted plugin: this one could only be published as private. Keep it?`,
4846
+ message: `${message} Keep it${availability.available ? "" : " anyway"}?`,
4660
4847
  initialValue: availability.available
4661
4848
  }))) return slug;
4662
4849
  suggestion = slug;
4663
4850
  }
4664
4851
  }
4852
+ function slugProblem(value, parentDir) {
4853
+ const parsed = PluginSlugSchema.safeParse(value);
4854
+ if (!parsed.success) return parsed.error.issues[0]?.message;
4855
+ return existsSync(join(parentDir, parsed.data)) ? `A folder named ${parsed.data} already exists in ${parentDir}.` : void 0;
4856
+ }
4857
+ function checkSlugFormat(slug, parentDir) {
4858
+ const problem = slugProblem(slug, parentDir);
4859
+ if (problem) fail(`Slug ${slug}: ${problem}`);
4860
+ return slug;
4861
+ }
4862
+ function nameProblem(value) {
4863
+ return PluginManifestSchema.shape.name.safeParse(value).success ? void 0 : "Use 1–50 characters.";
4864
+ }
4865
+ function checkName(name) {
4866
+ const problem = nameProblem(name);
4867
+ if (problem) fail(`Plugin name: ${problem}`);
4868
+ return name;
4869
+ }
4870
+ /** A flag's value if it's one of `options`; exits otherwise. */
4871
+ function parseChoice(flag, value, options) {
4872
+ if (value === void 0) return;
4873
+ if (!options.includes(value)) fail(`${flag} must be ${options.slice(0, -1).join(", ")} or ${options.at(-1)}.`);
4874
+ return value;
4875
+ }
4876
+ /** `--permissions storage,ui:notify`; `""` means none. */
4877
+ function parsePermissions(value) {
4878
+ if (value === void 0) return;
4879
+ const names = value.split(",").map((name) => name.trim()).filter(Boolean);
4880
+ const unknown = names.filter((name) => !PluginPermissionSchema.safeParse(name).success);
4881
+ if (unknown.length > 0) fail(`Unknown permission ${unknown.join(", ")}. Use ${PluginPermissionSchema.options.join(", ")}.`);
4882
+ return [...new Set(names)];
4883
+ }
4884
+ function fail(message) {
4885
+ log.error(message);
4886
+ process.exit(CLI_CONFIG.exitCodes.validation);
4887
+ }
4665
4888
  /** Best effort: `undefined` when not logged in or the API can't be reached. */
4666
4889
  async function checkSlug(apiUrl, slug) {
4667
4890
  const token = await getToken(apiUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namostack/cli",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.2",
4
4
  "description": "Create, live test and publish namostack plugins.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.namostack.com",
@@ -39,10 +39,10 @@
39
39
  "@types/semver": "^7.8.0",
40
40
  "tsdown": "^0.23.1",
41
41
  "typescript": "~6.0.2",
42
- "@namostack/config": "0.0.0",
43
- "@namostack/tsconfig": "0.0.0",
44
42
  "@namostack/api-client": "0.0.0",
45
- "@namostack/types": "0.0.0"
43
+ "@namostack/config": "0.0.0",
44
+ "@namostack/types": "0.0.0",
45
+ "@namostack/tsconfig": "0.0.0"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsdown",
@@ -1,4 +1,4 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
- <rect width="64" height="64" rx="14" fill="#4f46e5" />
2
+ <rect width="64" height="64" rx="14" fill="#6344e5" />
3
3
  <path d="M22 16h13a11 11 0 0 1 0 22h-6v10h-7zm7 6v10h6a5 5 0 0 0 0-10z" fill="#fff" fill-rule="evenodd" />
4
4
  </svg>