@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 +362 -139
- package/package.json +4 -4
- package/templates/react/public/icon.svg +1 -1
- package/templates/react/src/App.tsx +76 -2
- package/templates/react/src/NamostackProvider.tsx +9 -0
- package/templates/react/src/components/Counter.tsx +101 -0
- package/templates/react/src/components/ExampleCard.tsx +55 -0
- package/templates/react/src/components/Notify.tsx +41 -0
- package/templates/react/src/components/styles.ts +24 -0
- package/templates/react/src/errors.ts +10 -0
- package/templates/vanilla/index.html +50 -1
- package/templates/vanilla/package.json +2 -0
- package/templates/vanilla/public/icon.svg +1 -1
- package/templates/vanilla/src/main.ts +146 -5
- package/templates/vanilla/src/style.css +330 -0
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
|
-
/**
|
|
3957
|
-
const
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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) =>
|
|
4586
|
-
})).trim();
|
|
4587
|
-
const
|
|
4588
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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/
|
|
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="#
|
|
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>
|